12

Edit/Note: I really need an answer to this, and I'd like to stay with the "stock" Google Android API. I've created a +100 bounty on this but if I get a straightforward solution to this using the stock API in the next few days I'll add another +100, making it worth 200 points.

I'm experimenting with the Android CalendarView control. I made a little app with a button and a CalendarView in a NestedScrollView. I made the button and its margins really big so I could verify scrolling worked. On a Samsung Galaxy S5 running Android 6.01 it works fine. But on a Samsung S Duo (which is my intended target) running 4.2.2 there's no way to advance the month (notice no arrow next to the month)

Here's a screenshot from a Samsung S5 running Android 6.01

s5 screenshot

and here's one from a Samsung s Duo running 4.2.2

S Duo screenshot

The content_main.xml looks like this . . .

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <!-- This linear layout is because the scrollview can have only 1 direct child -->
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical">

        <Button
            android:id="@+id/recordEnd"
            android:layout_width="200dp"
            android:layout_height="100dp"
            android:layout_marginTop="100dp"
            android:layout_marginBottom="100dp"
            android:gravity="center"
            android:text="Record End"/>

        <CalendarView
            android:id="@+id/thecalendar"
            android:layout_width="240dp"
            android:layout_height="300dp"
            android:minDate="01/01/2016"
            android:maxDate="11/30/2016"/>
    </LinearLayout>
</android.support.v4.widget.NestedScrollView>

...In Android Studio the design view does show the arrows. I have no idea how to debug this.

user316117
  • 7,299
  • 18
  • 70
  • 141

2 Answers2

10

Having a quick look in CalendarView code it looks like the calendar appearance is inferred from the system and I wasn't able to find a way to change it. Below you can find how the calendar's view is selected depending on mode:

final TypedArray a = context.obtainStyledAttributes(
        attrs, R.styleable.CalendarView, defStyleAttr, defStyleRes);
final int mode = a.getInt(R.styleable.CalendarView_calendarViewMode, MODE_HOLO);
a.recycle();

switch (mode) {
    case MODE_HOLO:
        mDelegate = new CalendarViewLegacyDelegate(
                this, context, attrs, defStyleAttr, defStyleRes);
        break;
    case MODE_MATERIAL:
        mDelegate = new CalendarViewMaterialDelegate(
                this, context, attrs, defStyleAttr, defStyleRes);
        break;
    default:
        throw new IllegalArgumentException("invalid calendarViewMode attribute");
}

Edit:

After some more time spent on the problem I have some better explications:

  1. The preview is showing the view with arrows because you haven't set the proper API version. If you have a look at the below images you'll see that on pre-Lollipop versions the buttons are missing, and after it, everything is nice and good. To change the API version simply click on the button highlighted in red and select your desired API level to see an accurate preview of your xml on different platforms.

    KitKat version Android N version

  2. Why the arrows are missing from the prior versions of Material Design? Here, I wasn't able to find an answer given by someone from Google, but I believe that this is somehow related to the screen resolution (on older devices you don't have that much space to show the CalendarView like in newer Android versions and this is why they chose the ListView since it can display only a few rows and still being fully functional, whereas displaying with a ViewPager will crop some of the last lines).

    As I said in the original post, the style of the CalendarView is selected based on the availability of Material Design (Android 5.0+).

    The Legacy version (find it below) has a ListView and the next/previous buttons are missing. Because the calendar is shown using a ListView you can't scroll to the next month since the parent view of the CalendarView is a Scroll. To fix this you can find some explications and solutions here.

    <TextView android:id="@+android:id/month_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:paddingTop="10dip"
        android:paddingBottom="10dip"
        style="@android:style/TextAppearance.Medium" />
    
     <!-- This is the header representing the days of the week -->
    <LinearLayout android:id="@+android:id/day_names"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="6dip"
        android:layout_marginEnd="2dip"
        android:layout_marginStart="2dip"
        android:gravity="center" >
    
        <TextView android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:visibility="gone" />
    
        <TextView android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center" />
    
        <TextView android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center" />
    
        <TextView android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center" />
    
        <TextView android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center" />
    
        <TextView android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center" />
    
        <TextView android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center" />
    
        <TextView android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center" />
    
    </LinearLayout>
    
    <ImageView android:layout_width="match_parent"
        android:layout_height="1dip"
        android:scaleType="fitXY"
        android:gravity="fill_horizontal"
        android:src="?android:attr/dividerHorizontal" />
    
    <ListView android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:drawSelectorOnTop="false"
        android:cacheColorHint="@android:color/transparent"
        android:fastScrollEnabled="false"
        android:overScrollMode="never" />
    

    The Material Design version replaces the ListView with a ViewPager and adds the prev and next buttons.

    <android.widget.DayPickerViewPager
        android:id="@+id/day_picker_view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    
    <ImageButton
        android:id="@+id/prev"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:minWidth="48dp"
        android:minHeight="48dp"
        android:src="@drawable/ic_chevron_start"
        android:background="?attr/selectableItemBackgroundBorderless"
        android:contentDescription="@string/date_picker_prev_month_button"
        android:visibility="invisible" />
    
    <ImageButton
        android:id="@+id/next"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:minWidth="48dp"
        android:minHeight="48dp"
        android:src="@drawable/ic_chevron_end"
        android:background="?attr/selectableItemBackgroundBorderless"
        android:contentDescription="@string/date_picker_next_month_button"
        android:visibility="invisible" />
    

    At the moment, I wasn't able to find a way to style the CalendarView since the stylable attributes are removed from public APIs.

    The android.R.styleable class and its fields were removed from the public API, to better ensure forward-compatibility for applications.

    That being said, you can keep the old-look style for older Android versions since you can't change it, but keep in mind that a ListView should not be in a Scroll or try to find a repository that is stable and fits better your needs.

Edit II:

The CalendarView is not broken, it works perfectly on all Android version, even if, it may give the impression that its behaviour is broken because of the implementation using a ListView. Now' let's dig a little in the problem such that everybody can understand what's happening:

  1. Problem no. 1: using a ListView in ScrollView
    Generally speaking it's a bad practice to use those elements nested because they will not be able to handle the scroll action properly.

  2. Problem no. 2: using a NestedScrollView doesn't fix the problem
    From Android 5.0 Google added that support library to support nested scrolling, but a ListView will not be able to handle the scroll unless it implements NestedScrollingChild. To do this subclass the ListView class and implement the interface methods and from each method call the corresponding method from NestedScrollingChildHelper. Following this steps you can use a ListView in a NestedScrollView, but not a CalendarView (see problem no. 3)

  3. Problem no. 3: you can't change the list from CalendarView
    The real problem is that the CalendarView doesn't have any methods that would allow you to change the default list with a custom list written by you. Supposing that you would have such a method, you could make the CalendarView work by replacing the default list with you custom one and the scroll would work perfectly, but unfortunately you can't.

To solve the problem you have 2 options:
- redesign you view such that the CalendarView isn't in a class that contains Scroll in its name
- use Pravin Divraniya's answer which will make you calendar work perfectly, but keep in mind that nested scrolls are bad (especially on older Android devices where the screen is small)

Community
  • 1
  • 1
Iulian Popescu
  • 2,347
  • 4
  • 20
  • 29
  • This interesting but I don't understand it. Does one of those two delegates have arrows and the other one doesn't? It doesn't make any sense that Android would have a native calendar that you can't change dates on regardless of "mode". I'll update my question to note that I will give 200 points for a straightforward solution to this using the Google Android API now so I don't have to wait for the bounty period to start. – user316117 Jul 07 '16 at 14:16
  • The Github example above says "_CalendarMode.WEEK and all week mode functionality is officially marked `@Experimental. All APIs marked `@Experimental are subject to change quickly and should not be used in production code. They are allowed for testing and feedback._" My application - once I get it working - is for production use in an industrial environment so I'd really prefer to stay with the stock API. CalendarView was added in API 11 and Android 4.2.2 is API 17 so I shouldn't need to consider it an older version. – user316117 Jul 07 '16 at 14:32
  • Thank you for all the background research, but I'm unclear how it might lead to a solution for my problem of how to get the native CalendarView to work properly with Android 4.2.2. Are you saying that the native API version is fundamentally flawed and this can never work? I.e., that for 4.2.2 it's always necessary to write a custom CalendarView even though the Google documentation says CalendarView exists since API 11? I can't possible be the first person to ever use CalendarView! – user316117 Jul 08 '16 at 19:29
  • ...also, I'm not sure how the link you provided helps. I was aware that that controls with built-in scrolling could not be placed in a ScrollView, which is why I used a NestedScrollView. Again, thanks for your research but I'm unsure how to apply it to my question. – user316117 Jul 08 '16 at 19:32
  • The link provided was to show that a `ListView` doesn't work well with a `ScrollView`. Even if now are some APIs that enable this thing http://stackoverflow.com/questions/6210895/listview-inside-scrollview-is-not-scrolling-on-android, this won't work with the `CalendarView` since you can't change that `ListView`. A workaround for the `CalendarView` would be to intercept the touches http://stackoverflow.com/questions/18782919/calendarview-stops-scrolling-when-nested-in-scrolview-in-android or to try redesign your layouts. – Iulian Popescu Jul 11 '16 at 10:11
  • Did you find any problems after my last comment? – Iulian Popescu Jul 12 '16 at 16:09
  • Maybe it's a language problem between us, but I still don't understand your conclusion. Are you saying that the native Android API CalendarView does not work properly prior to Android 5, even though it's been out since API 11? (and I'm the first person to enconter this?) And so the only way to get CalendarView-like functionality is to derive my own class from CalendarView and override its message-handling? – user316117 Jul 13 '16 at 21:21
  • I updated the answer and I hope that the things are better explained now. – Iulian Popescu Jul 14 '16 at 12:31
  • I'm trying Pravin's solution now - I'll report back in a few days on my results . . . – user316117 Jul 18 '16 at 17:41
  • Pravin's solution worked. I'm disappointed that native Android classes clash with each other straight out of the tin so I have to override them just to make them play well together. I'm confused about the S.O. bounty scheme - I posted a question to meta : [http://meta.stackoverflow.com/questions/328262/need-clarification-on-a-bounty-trying-to-piece-together-what-happened](http://meta.stackoverflow.com/questions/328262/need-clarification-on-a-bounty-trying-to-piece-together-what-happened). Once I get that sorted I will try to make sure both of you get some rep points for your work. – user316117 Jul 19 '16 at 18:01
  • There doesn't seem to be a way to do this on StackExchange sites (you can do it on other sites) - this is very annoying because both of the answers were helpful, just in different ways and lulian put a lot of work and digging into his answer and deserves some kind of a reward, even though I didn't accept it as the answer. To express my gratitude, lulian, I've upvoted some of your other answers that I thought were helpful on other Android questions. – user316117 Jul 20 '16 at 14:15
8

A nice and deep explanation by Iulian Popescu, I am just trying to simplify it. First of all I want to clarify few things.

  1. According to this official Android developer link, The exact appearance and interaction model of CalenderView widget may vary between OS versions and themes (e.g. Holo versus Material).
  2. As Iulian Popescu pasted the code from android.widget.CalendarView class of Android, You can see that CalendarViewLegacyDelegate class is responsible for rendering CalenderView in case of HOLO theme and CalendarViewMaterialDelegate class is responsible for rendering CalenderView in case of MATERIAL theme. I am again posting that code for just reference purpose.
public CalendarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.CalendarView, defStyleAttr, defStyleRes);
        final int mode = a.getInt(R.styleable.CalendarView_calendarViewMode, MODE_HOLO);
        a.recycle();
        switch (mode) {
            case MODE_HOLO:
                mDelegate = new CalendarViewLegacyDelegate(
                        this, context, attrs, defStyleAttr, defStyleRes);
                break;
            case MODE_MATERIAL:
                mDelegate = new CalendarViewMaterialDelegate(
                        this, context, attrs, defStyleAttr, defStyleRes);
                break;
            default:
                throw new IllegalArgumentException("invalid calendarViewMode attribute");
        }
    }
  1. In CalendarViewMaterialDelegate(android.widget.CalendarViewMaterialDelegate) class we can see that DayPickerView(android.widget.DayPickerView) class is used as a container to render the calender view that we can see in Material theme.In DayPickerView class ViewPager used to display one month in one page. Also two ImageButton for next and previous. As there is two buttons to change the month, it will not create any problem and works just fine in MATERIAL theme.
    private final ViewPager mViewPager;
    private final ImageButton mPrevButton;
    private final ImageButton mNextButton;

  1. As we can see in CalendarViewLegacyDelegate(android.widget.CalendarViewLegacyDelegate) class, ListView is used to display list of weeks(Vertical scrolling) in CalenderView. There is no any ImageButton for previous and next as it will scroll vertically.
/**
     * The adapter for the weeks list.
     */
    private WeeksAdapter mAdapter;

    /**
     * The weeks list.
     */
    private ListView mListView;

As per this link of Android developer website, We should never use a ScrollView with a ListView, because ListView takes care of its own vertical scrolling. Because of this in case of HOLO theme CalenderView will behave unexpectedly when using it inside ScrollView.

Solution:-

You can use below given custom scroll view class instead of NestedScrollView class. It will make your CalenderView vertical scrolling smooth in case of HOLO theme.

public class VerticalScrollView extends ScrollView{

    public VerticalScrollView(Context context) {
        super(context);
    }

    public VerticalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public VerticalScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                super.onTouchEvent(ev);
                break;

            case MotionEvent.ACTION_MOVE:
                return false; // redirect MotionEvents to ourself

            case MotionEvent.ACTION_CANCEL:
                super.onTouchEvent(ev);
                break;

            case MotionEvent.ACTION_UP:
                return false;

            default: 
                break;
        }

        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        super.onTouchEvent(ev);
        return true;
    }
}

I hope this will clear your doubts.

Community
  • 1
  • 1
Pravin Divraniya
  • 3,642
  • 2
  • 26
  • 45