Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where did the arrows go?

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.

like image 770
user316117 Avatar asked Jul 06 '16 21:07

user316117


2 Answers

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)

like image 58
Iulian Popescu Avatar answered Oct 24 '22 18:10

Iulian Popescu


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.

like image 33
Pravin Divraniya Avatar answered Oct 24 '22 17:10

Pravin Divraniya