CoordinateLayout can coordinate its sub-layout to realize the linkage of sliding effect, and its sliding effect is realized by Behavior. I have used xiaomi calendar before and was impressed by the effect of sliding smoothly between sun and moon views. This article attempts to implement a calendar with this effect using custom Behavior.

Introduction to the

First on the millet calendar figure, let you know what effect to do:

This is the effect of Xiaomi calendar. When users operate the list, the calendar can be folded into a weekly view, which expands the display area of the list and does not affect the use of calendar functions. It is interesting and practical.

Use below CoordinateLayout behaviors, simple to achieve a similar effect.

Calendar control

I’m not going to write another calendar control myself. I wanted to use native CalendarView, but CalendarView doesn’t support weekly views and doesn’t have a high degree of customization.

I did a GitHub search and decided to use MaterialCalendarView. This is a popular library that supports switching between week-to-month views, complies with Material Design, and allows you to customize the display.

Introduce the library to use in layout files:

<com.prolificinteractive.materialcalendarview.MaterialCalendarView
    android:id="@+id/calendar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:mcv_showOtherDates="all" />
Copy the code

The code for switching views is as follows:

calendarView.state().edit()
    .setCalendarDisplayMode(CalendarMode.WEEKS)
    .commit();
Copy the code

Behavior

Before you start writing code, there are a few things you need to know.

Using CoordinatorLayout as the root layout, you can coordinate the interaction between its child controls, which is realized by its internal class Behavior. In the layout, the app:layout_behavior property is configured for the child controls to achieve the corresponding linkage effect. So here we need to customize two behaviors for calendar and list.

Behavior can be associated in two ways. One is by creating dependencies, and one is by nesting a sliding mechanism like RecyclerView or NextedScrollView, which WE’ll talk about later. We should first analyze the effect we want to achieve, determine the dependency between each child control, avoid cyclic dependency and other errors.

In addition, the layout of CoordinatorLayout is similar to FrameLayout, so you need to consider the placement of the controls.

Folding effect

You may have seen the linkage effect of RecyclerView and AppBarLayout, this effect needs to configure the Behavior of RecyclerView:

app:layout_behavior="@string/appbar_scrolling_view_behavior"
Copy the code

But why only RecyclerView and not AppBarLayout? Take a look at the AppBarLayout source code to know, it has given itself by default:

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
    // ...
}
Copy the code

If you look at the Behavior source code, it inherits ViewOffsetBehavior. ViewOffsetBehavior is used to easily change the position of the control and get offsets. So I’m going to be a little lazy here, and I’m going to copy ViewOffsetBehavior directly from the source code.

We customize two behaviors, the list control’s CalendarScrollBehavior and the calendar control’s CalendarBehavior, which inherit from the ViewOffsetBehavior.

CalendarScrollBehavior

In Behavior, the layoutDependsOn method is used to establish a dependency. A control can depend on more than one other control, but it cannot be cyclically dependent. The onDependentViewChanged method is called when the property of a dependent control changes.

So to reduce the complexity, I’m going to do all the folding in a CalendarBehavior, and one thing I do in a CalendarScrollBehavior is I put the list under the calendar. CalendarScrollBehavior code:

public class CalendarScrollBehavior extends ViewOffsetBehavior<RecyclerView> {

    private int calendarHeight;

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

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {
        return dependency instanceof MaterialCalendarView;
    }

    @Override
    protected void layoutChild(CoordinatorLayout parent, RecyclerView child, int layoutDirection) {
        super.layoutChild(parent, child, layoutDirection);
        if (calendarHeight == 0) {
            final List<View> dependencies = parent.getDependencies(child);
            for (int i = 0, z = dependencies.size(); i < z; i++) {
                View view = dependencies.get(i);
                if (view instanceofMaterialCalendarView) { calendarHeight = view.getMeasuredHeight(); } } } child.setTop(calendarHeight); child.setBottom(child.getBottom() + calendarHeight); }}Copy the code

The onDependentViewChanged method is not used here, and all linkage actions are implemented through nested sliding mechanisms.

CalendarBehavior

Next comes the focus of this article. The nested sliding mechanism we use mainly involves the following methods:

  • onStartNestedScroll
  • onNestedPreScroll
  • onStopNestedScroll
  • onNestedPreFling

When the RecyclerView or NestedScrollView slides, the CoordinatorLayout’s child control Behavior can receive the corresponding callback. The name of the method should give you a sense of its purpose, which will be discussed below.

The return value of onStartNestedScroll determines whether nested slide events are received. We decide that as long as we swipe up and down, we receive:

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                                   MaterialCalendarView child,
                                   View directTargetChild,
                                   View target,
                                   int axes, int type) {
    return(axes & ViewCompat.SCROLL_AXIS_VERTICAL) ! =0;
}
Copy the code

The onNestedPreScroll method is called before you are ready to scroll, with the scroll offset dy.

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout,
                              final MaterialCalendarView child,
                              View target,
                              int dx, int dy,
                              int[] consumed,
                              int type)                            
Copy the code

All we have to do, at the right time, is consume this offset and turn it into a folding effect.

Analyze the folding effect. When you scroll, the calendar also scrolls up, up to the current selected date line, the scroll range and the current selected date. Moving up is negative, so the calendar can scroll from 0 to -calendarlineHeight * (weekofmonth-1), minus 1 to leave one more row showing the week’s title. The list has a fixed scrolling range of up to 5 calendar line heights, from 0 to -calendarlineheight * 5.

To determine if the offset is in this range, change the position of the control using the setTopAndBottomOffset method of ViewOffsetBehavior. So you have to get the CalendarScrollBehavior. GetLayoutParams ()).getBehavior() to get CalendarScrollBehavior.

So when you fold, you consume offsets, and that’s using consumed parameter, which is an array of length two that contains the x and y offsets that you consume.

The final code is as follows:

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout,
                              final MaterialCalendarView child,
                              View target,
                              int dx, int dy,
                              int[] consumed,
                              int type) {
    // The list does not slide to the top
    if (target.canScrollVertically(-1)) {
        return;
    }
    // Switch the monthly view
    setMonthMode(child);
    if (calendarMode == CalendarMode.MONTHS) {
        if (calendarLineHeight == 0) {
            calendarLineHeight = child.getMeasuredHeight() / 7;
            weekCalendarHeight = calendarLineHeight * 2;
            monthCalendarHeight = calendarLineHeight * 7;
            listMaxOffset = calendarLineHeight * 5;
        }
        // Move calendar
        int calendarMinOffset = -calendarLineHeight * (weekOfMonth - 1);
        int calendarOffset = MathUtils.clamp(
            getTopAndBottomOffset() - dy, calendarMinOffset, 0);
        setTopAndBottomOffset(calendarOffset);
        // Move the list
        final CoordinatorLayout.Behavior behavior =
                ((CoordinatorLayout.LayoutParams) target.getLayoutParams()).getBehavior();
        if (behavior instanceof CalendarScrollBehavior) {
            final CalendarScrollBehavior listBehavior = (CalendarScrollBehavior) behavior;
            int listMinOffset = -listMaxOffset;
            int listOffset = MathUtils.clamp(
                listBehavior.getTopAndBottomOffset() - dy, -listMaxOffset, 0);
            listBehavior.setTopAndBottomOffset(listOffset);
            if (listOffset > -listMaxOffset && listOffset < 0) {
                consumed[1] = dy; }}}}Copy the code

Now we can match the layout parameters to see the effect. The layout is as follows:

<?xml version="1.0" encoding="utf-8"? >
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.prolificinteractive.materialcalendarview.MaterialCalendarView
        android:id="@+id/calendar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/calendar_behavior"
        app:mcv_showOtherDates="all" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="110dp"
        android:background="@color/color_ee"
        app:layout_behavior="@string/calendar_scrolling_behavior" />

</android.support.design.widget.CoordinatorLayout>
Copy the code

When selecting other dates, be sure to inform Behvior of the week of the month:

calendarView.setOnDateChangedListener(new OnDateSelectedListener() {
    @Override
    public void onDateSelected(MaterialCalendarView widget,
                               CalendarDay date,
                               boolean selected) { Calendar calendar = date.getCalendar(); calendarBehavior.setWeekOfMonth(calendar.get(Calendar.WEEK_OF_MONTH)); }});Copy the code

The effect is as follows:

Week title

As you can see above, the title showing the week has also moved up, and there is no way to hide this title from the MaterialCalendarView.

No way, had to write a week of their own title control cover above, simply wrote a WeekTitleView, code is not posted, in the layout with:

<?xml version="1.0" encoding="utf-8"? >
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.prolificinteractive.materialcalendarview.MaterialCalendarView
        android:id="@+id/calendar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/calendar_behavior"
        app:mcv_showOtherDates="all" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="110dp"
        android:background="@color/color_ee"
        app:layout_behavior="@string/calendar_scrolling_behavior"
        tools:listitem="@layout/item_list" />

    <com.southernbox.nestedcalendar.view.WeekTitleView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#fafafa" />

</android.support.design.widget.CoordinatorLayout>
Copy the code

The effect is as follows:

Smooth switching view

Next deal with the issue of week-to-month view switching.

When the nested slide ends, the onStopNestedScroll method is called to determine whether to switch views based on the position of the current control. When you swipe to the top you switch to a weekly view, and everything else is a monthly view:

@Override
public void onStopNestedScroll(final CoordinatorLayout coordinatorLayout,
                               final MaterialCalendarView child,
                               final View target,
                               int type) {
    if (calendarLineHeight == 0) {
        return;
    }
    if (target.getTop() == weekCalendarHeight) {
        setWeekMode(child);
    } else{ setMonthMode(child); }}Copy the code

The effect is as follows:

The view switch of MaterialCalendarView is a little bit sluggish, but acceptable.

Inertial sliding

You can see a problem with the above effect. When you let go halfway through the slide, you should return to the full view position. There are two kinds of effects: inertial sliding to a specified position after a quick slide, and sliding to the nearest specified position without a quick slide.

The return value of the onNestedPreFling method determines whether inertial nested slides are performed:

@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout,
                                MaterialCalendarView child,
                                View target,
                                float velocityX, float velocityY) {
    this.velocityY = velocityY;
    return! (target.getTop() == weekCalendarHeight || target.getTop() == monthCalendarHeight); }Copy the code

Judge and perform scroll in onStopNestedScroll. Since our rolled-fold effect is implemented in onNestedPreScroll, we need to find a way to trigger this method. OnNestedPreScroll is called in dispatchNestedPreScroll if startNestedScroll is true. So it can be triggered like this:

recyclerView.startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, TYPE_TOUCH);
recyclerView.dispatchNestedPreScroll(0, dy, new int[2].new int[2], TYPE_TOUCH);
Copy the code

The complete code for onStopNestedScroll is as follows:

@Override
public void onStopNestedScroll(final CoordinatorLayout coordinatorLayout,
                               final MaterialCalendarView child,
                               final View target,
                               int type) {
    if (calendarLineHeight == 0) {
        return;
    }
    if (target.getTop() == weekCalendarHeight) {
        setWeekMode(child);
        return;
    } else if (target.getTop() == monthCalendarHeight) {
        setMonthMode(child);
        return;
    }
    if(! canAutoScroll) {return;
    }
    if (calendarMode == CalendarMode.MONTHS) {
        final Scroller scroller = new Scroller(coordinatorLayout.getContext());
        int offset;
        int duration = 800;
        if (Math.abs(velocityY) < 1000) {
            if (target.getTop() > calendarLineHeight * 4) {
                offset = monthCalendarHeight - target.getTop();
            } else{ offset = weekCalendarHeight - target.getTop(); }}else {
            if (velocityY > 0) {
                offset = weekCalendarHeight - target.getTop();
            } else {
                offset = monthCalendarHeight - target.getTop();
            }
        }
        velocityY = 0;
        duration = duration * Math.abs(offset) / (listMaxOffset);
        scroller.startScroll(
                0, target.getTop(),
                0, offset,
                duration);
        ViewCompat.postOnAnimation(child, new Runnable() {
            @Override
            public void run(a) {
                if (scroller.computeScrollOffset() &&
                        target instanceof RecyclerView) {
                    canAutoScroll = false;
                    RecyclerView recyclerView = (RecyclerView) target;
                    int delta = target.getTop() - scroller.getCurrY();
                    recyclerView.startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, TYPE_TOUCH);
                    recyclerView.dispatchNestedPreScroll(
                            0, delta, new int[2].new int[2], TYPE_TOUCH);
                    ViewCompat.postOnAnimation(child, this);
                } else {
                    canAutoScroll = true;
                    if (target.getTop() == weekCalendarHeight) {
                        setWeekMode(child);
                    } else if(target.getTop() == monthCalendarHeight) { setMonthMode(child); }}}}); }}Copy the code

At this point, the custom Behavior is complete.

The effect

Take a look at the final result:

The advantage of this implementation is less code and convenient to use. Using MaterialCalendarView and not modifying its source code means that all its functions are supported.

I hope that through this article, you can have a general understanding of Behavior.

The project address