热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

自定义Behavior,实现嵌套滑动、平滑切换周月视图的日历

使用CoordinateLayout可以协调它的子布局,实现滑动效果的联动,它的滑动效果由Behavior实现。以前用过小米日历,对它滑动

使用 CoordinateLayout 可以协调它的子布局,实现滑动效果的联动,它的滑动效果由 Behavior 实现。以前用过小米日历,对它滑动平滑切换日月视图的效果印象深刻。本文尝试用自定义 Behavior 实现一个带有这种效果的日历。

简介

先上个小米日历的图,让大家知道要做一个什么效果:

这是小米日历的效果,在用户操作列表的时候,将日历折叠成周视图,扩大列表的显示区域,同时也不影响日历部分的功能使用,有趣且实用。

下面利用 CoordinateLayout.Behavior,简单实现一个类似的效果。

日历控件

我并不打算自己再写一个日历控件。原本想用原生的 CalendarView,但是 CalendarView 不支持周视图,可自定义程度也不高。

在 GitHub 搜了一下,决定使用 MaterialCalendarView。这个库比较流行,它支持周月视图的切换,符合 Material Design,也可以自定义显示效果。

引入该库,在布局文件中使用:

<com.prolificinteractive.materialcalendarview.MaterialCalendarViewandroid:id&#61;"&#64;&#43;id/calendar"android:layout_width&#61;"match_parent"android:layout_height&#61;"wrap_content"app:mcv_showOtherDates&#61;"all" />

切换视图代码如下&#xff1a;

calendarView.state().edit().setCalendarDisplayMode(CalendarMode.WEEKS).commit();

Behavior

写代码之前&#xff0c;还有些东西需要先了解一下。

用 CoordinatorLayout 作为根布局&#xff0c;就可以协调它子控件之间的联动效果&#xff0c;至于如何联动&#xff0c;是由它的内部类 Behavior 实现的。在布局中&#xff0c;对子控件配置 app:layout_behavior 属性&#xff0c;实现对应的联动效果。所以这里我们需要自定义日历和列表的两个 Behavior。

Behavior 有两种实现联动的方式。一种是通过建立依赖关系&#xff0c;一种是通过 RecyclerView 或 NextedScrollView 的嵌套滑动机制&#xff0c;后面都会讲到。我们要先分析想要实现的效果&#xff0c;确定各个子控件之间的依赖关系&#xff0c;避免循环依赖等错误。

另外&#xff0c;由于 CoordinatorLayout 的布局类似于 FrameLayout&#xff0c;所以还需要考虑摆放控件位置的问题。

折叠效果

大家可能有看过 RecyclerView 和 AppBarLayout 联动的效果&#xff0c;这种效果需要给 RecyclerView 配置 Behavior&#xff1a;

app:layout_behavior&#61;"&#64;string/appbar_scrolling_view_behavior"

但为什么只要给 RecyclerView 配不用给 AppBarLayout 配&#xff1f;看一下 AppBarLayout 的源码就知道了&#xff0c;它默认已经给自己配了&#xff1a;

&#64;CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {// ...
}

看它 Behavior 源码发现&#xff0c;它继承了 ViewOffsetBehavior。ViewOffsetBehavior 的作用是方便改变控件的位置和获取偏移量。所以这里我再偷个懒&#xff0c;把源码里的 ViewOffsetBehavior 直接拷出来用了。

我们自定义两个 Behavior&#xff0c;列表控件的 CalendarScrollBehavior 和日历控件的 CalendarBehavior&#xff0c;都继承 ViewOffsetBehavior。

CalendarScrollBehavior

在 Behavior 中&#xff0c;通过 layoutDependsOn 方法来建立依赖关系&#xff0c;一个控件可以依赖多个其他控件&#xff0c;但不可循环依赖。当被依赖的控件属性发生变化时&#xff0c;会调用 onDependentViewChanged 方法。

为了降低复杂程度&#xff0c;我将所有折叠操作都放到 CalendarBehavior 里做&#xff0c;而 CalendarScrollBehavior 里面做一件事&#xff0c;就是把列表置于日历之下。参考了源码 ScrollingViewBehavior&#xff0c;CalendarScrollBehavior 代码如下&#xff1a;

public class CalendarScrollBehavior extends ViewOffsetBehavior<RecyclerView> {private int calendarHeight;public CalendarScrollBehavior(Context context, AttributeSet attrs) {super(context, attrs);}&#64;Overridepublic boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {return dependency instanceof MaterialCalendarView;}&#64;Overrideprotected void layoutChild(CoordinatorLayout parent, RecyclerView child, int layoutDirection) {super.layoutChild(parent, child, layoutDirection);if (calendarHeight &#61;&#61; 0) {
final List<View> dependencies &#61; parent.getDependencies(child);
for (int i &#61; 0, z &#61; dependencies.size(); i < z; i&#43;&#43;) {View view &#61; dependencies.get(i);if (view instanceof MaterialCalendarView) {calendarHeight &#61; view.getMeasuredHeight();}
}}child.setTop(calendarHeight);child.setBottom(child.getBottom() &#43; calendarHeight);}
}

这里没有用到 onDependentViewChanged 方法&#xff0c;所有联动操作都将通过嵌套滑动机制实现。

CalendarBehavior

接下来是本文的重点&#xff0c;我们使用的嵌套滑动机制&#xff0c;主要涉及到以下几个方法&#xff1a;

  • onStartNestedScroll

  • onNestedPreScroll

  • onStopNestedScroll

  • onNestedPreFling

当 RecyclerView 或 NestedScrollView 滑动时&#xff0c;CoordinatorLayout 的子控件 Behavior 可以接收到对应的回调。看方法名应该大概知道它的用途了&#xff0c;下面都会提到。

onStartNestedScroll 的返回值决定是否接收嵌套滑动事件。我们判断&#xff0c;只要是上下滑动&#xff0c;就接收&#xff1a;

&#64;Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,MaterialCalendarView child,View directTargetChild,View target,int axes, int type) {return (axes &amp; ViewCompat.SCROLL_AXIS_VERTICAL) !&#61; 0;
}

onNestedPreScroll 这个方法是在准备滚动之前调用的&#xff0c;它带有滚动偏移量 dy。

&#64;Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout,final MaterialCalendarView child,View target,int dx, int dy,int[] consumed,int type)

我们要做的&#xff0c;就是在恰当的时候&#xff0c;消费掉这个偏移量&#xff0c;转化成折叠的效果。

分析一下这个折叠效果。滚动时&#xff0c;日历也向上滚动&#xff0c;最多到当前选中日期那一行&#xff0c;滚动范围和当前选中日期有关。向上移动是负值&#xff0c;所以日历的滚动范围是从 0 到 -calendarLineHeight * (weekOfMonth - 1)&#xff0c;减 1 是因为要多留一行显示星期的标题。列表的滚动范围则是固定的&#xff0c;最多向上移动 5 倍的日历行高&#xff0c;也就是从 0 到 -calendarLineHeight * 5。

判断偏移量是否在这个范围内&#xff0c;用 ViewOffsetBehavior 的 setTopAndBottomOffset 方法来改变控件位置。所以还要拿到 CalendarScrollBehavior 进行操作。参数 target 是触发嵌套滑动的控件&#xff0c;在这里就是 RecyclerView&#xff0c;通过 target.getLayoutParams()).getBehavior() 就可以拿到 CalendarScrollBehavior 了。

折叠过程中&#xff0c;要将偏移量消费掉&#xff0c;这就用到了 consumed 这个参数&#xff0c;它是一个长度为 2 的数组&#xff0c;存放的是要消费掉的 x 和 y 轴偏移量。

最终代码如下&#xff1a;

&#64;Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout,final MaterialCalendarView child,View target,int dx, int dy,int[] consumed,int type) {// 列表未滑动到顶部时&#xff0c;不处理if (target.canScrollVertically(-1)) {return;}// 切换月视图setMonthMode(child);if (calendarMode &#61;&#61; CalendarMode.MONTHS) {if (calendarLineHeight &#61;&#61; 0) {
calendarLineHeight &#61; child.getMeasuredHeight() / 7;
weekCalendarHeight &#61; calendarLineHeight * 2;
monthCalendarHeight &#61; calendarLineHeight * 7;
listMaxOffset &#61; calendarLineHeight * 5;}// 移动日历int calendarMinOffset &#61; -calendarLineHeight * (weekOfMonth - 1);int calendarOffset &#61; MathUtils.clamp(
getTopAndBottomOffset() - dy, calendarMinOffset, 0);setTopAndBottomOffset(calendarOffset);// 移动列表final CoordinatorLayout.Behavior behavior &#61;((CoordinatorLayout.LayoutParams) target.getLayoutParams()).getBehavior();if (behavior instanceof CalendarScrollBehavior) {
final CalendarScrollBehavior listBehavior &#61; (CalendarScrollBehavior) behavior;
int listMinOffset &#61; -listMaxOffset;
int listOffset &#61; MathUtils.clamp(listBehavior.getTopAndBottomOffset() - dy, -listMaxOffset, 0);
listBehavior.setTopAndBottomOffset(listOffset);
if (listOffset > -listMaxOffset &amp;&amp; listOffset < 0) {consumed[1] &#61; dy;
}}}
}

现在我们可以把布局参数配一下&#xff0c;看一下效果了&#xff0c;布局如下&#xff1a;


<android.support.design.widget.CoordinatorLayoutxmlns:android&#61;"http://schemas.android.com/apk/res/android"xmlns:app&#61;"http://schemas.android.com/apk/res-auto"android:layout_width&#61;"match_parent"android:layout_height&#61;"match_parent"><com.prolificinteractive.materialcalendarview.MaterialCalendarViewandroid:id&#61;"&#64;&#43;id/calendar"android:layout_width&#61;"match_parent"android:layout_height&#61;"wrap_content"app:layout_behavior&#61;"&#64;string/calendar_behavior"app:mcv_showOtherDates&#61;"all" /><android.support.v7.widget.RecyclerViewandroid:id&#61;"&#64;&#43;id/recycler_view"android:layout_width&#61;"match_parent"android:layout_height&#61;"match_parent"android:layout_marginBottom&#61;"110dp"android:background&#61;"&#64;color/color_ee"app:layout_behavior&#61;"&#64;string/calendar_scrolling_behavior" />android.support.design.widget.CoordinatorLayout>

在选中其他日期的时候&#xff0c;记得通知 Behvior 选中的是该月的第几个星期&#xff1a;

calendarView.setOnDateChangedListener(new OnDateSelectedListener() {&#64;Overridepublic void onDateSelected(MaterialCalendarView widget,CalendarDay date,boolean selected) {Calendar calendar &#61; date.getCalendar();calendarBehavior.setWeekOfMonth(calendar.get(Calendar.WEEK_OF_MONTH));}
});

效果如下&#xff1a;

星期标题

上面效果可以看到&#xff0c;显示星期的标题也一起向上移动了&#xff0c;而且 MaterialCalendarView 是没办法隐藏这个标题的。

没办法&#xff0c;只好自己写一个星期标题的控件盖在上面&#xff0c;简单写了一个 WeekTitleView&#xff0c;代码就不贴了&#xff0c;在布局里加上&#xff1a;


<android.support.design.widget.CoordinatorLayoutxmlns:android&#61;"http://schemas.android.com/apk/res/android"xmlns:app&#61;"http://schemas.android.com/apk/res-auto"xmlns:tools&#61;"http://schemas.android.com/tools"android:layout_width&#61;"match_parent"android:layout_height&#61;"match_parent"><com.prolificinteractive.materialcalendarview.MaterialCalendarViewandroid:id&#61;"&#64;&#43;id/calendar"android:layout_width&#61;"match_parent"android:layout_height&#61;"wrap_content"app:layout_behavior&#61;"&#64;string/calendar_behavior"app:mcv_showOtherDates&#61;"all" /><android.support.v7.widget.RecyclerViewandroid:id&#61;"&#64;&#43;id/recycler_view"android:layout_width&#61;"match_parent"android:layout_height&#61;"match_parent"android:layout_marginBottom&#61;"110dp"android:background&#61;"&#64;color/color_ee"app:layout_behavior&#61;"&#64;string/calendar_scrolling_behavior"tools:listitem&#61;"&#64;layout/item_list" /><com.southernbox.nestedcalendar.view.WeekTitleViewandroid:layout_width&#61;"match_parent"android:layout_height&#61;"wrap_content"android:background&#61;"#fafafa" />android.support.design.widget.CoordinatorLayout>

效果如下&#xff1a;

平滑切换视图

接下来处理周月视图切换的问题。

当嵌套滑动结束时会回调 onStopNestedScroll 方法&#xff0c;可以在这里根据当前控件的位置&#xff0c;判断是否要切换视图。当滑动到最上面的时候切换为周视图&#xff0c;其余的情况都是月视图&#xff1a;

&#64;Override
public void onStopNestedScroll(final CoordinatorLayout coordinatorLayout,final MaterialCalendarView child,final View target,int type) {if (calendarLineHeight &#61;&#61; 0) {return;}if (target.getTop() &#61;&#61; weekCalendarHeight) {setWeekMode(child);} else {setMonthMode(child);}
}

效果如下&#xff1a;

MaterialCalendarView 的视图切换会有一点点卡顿&#xff0c;但还是能接受的。

惯性滑动

上面效果可以看出一个问题&#xff0c;当滑动到一半的时候松手&#xff0c;应该要恢复到完整视图的位置。这里包含了&#xff0c;快速滑动后惯性滑动到指定位置的效果&#xff0c;和没有快速滑动时&#xff0c;往就近的指定位置滑动这两种效果。

我们可以从 onNestedPreFling 拿到滑动速度&#xff0c;方法的返回值决定了是否进行惯性嵌套滑动&#xff1a;

&#64;Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout,MaterialCalendarView child,View target,float velocityX, float velocityY) {this.velocityY &#61; velocityY;return !(target.getTop() &#61;&#61; weekCalendarHeight ||
target.getTop() &#61;&#61; monthCalendarHeight);
}

在 onStopNestedScroll 里判断并执行滚动。由于我们的滚动折叠效果是在 onNestedPreScroll 实现的&#xff0c;所以要想办法触发这个方法。通过源码可以知道&#xff0c;onNestedPreScroll 是在 dispatchNestedPreScroll 里调用的&#xff0c;前提是 startNestedScroll 为 true。所以可以这样触发&#xff1a;

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

最终 onStopNestedScroll 的完整代码如下&#xff1a;

&#64;Override
public void onStopNestedScroll(final CoordinatorLayout coordinatorLayout,final MaterialCalendarView child,final View target,int type) {if (calendarLineHeight &#61;&#61; 0) {return;}if (target.getTop() &#61;&#61; weekCalendarHeight) {setWeekMode(child);return;} else if (target.getTop() &#61;&#61; monthCalendarHeight) {setMonthMode(child);return;}if (!canAutoScroll) {return;}if (calendarMode &#61;&#61; CalendarMode.MONTHS) {final Scroller scroller &#61; new Scroller(coordinatorLayout.getContext());int offset;int duration &#61; 800;if (Math.abs(velocityY) < 1000) {
if (target.getTop() > calendarLineHeight * 4) {offset &#61; monthCalendarHeight - target.getTop();
} else {offset &#61; weekCalendarHeight - target.getTop();
}} else {
if (velocityY > 0) {offset &#61; weekCalendarHeight - target.getTop();
} else {offset &#61; monthCalendarHeight - target.getTop();
}}velocityY &#61; 0;duration &#61; duration * Math.abs(offset) / (listMaxOffset);scroller.startScroll(0, target.getTop(),0, offset,duration);ViewCompat.postOnAnimation(child, new Runnable() {
&#64;Override
public void run() {if (scroller.computeScrollOffset() &amp;&amp;
target instanceof RecyclerView) {canAutoScroll &#61; false;RecyclerView recyclerView &#61; (RecyclerView) target;int delta &#61; 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 &#61; true;if (target.getTop() &#61;&#61; weekCalendarHeight) {
setWeekMode(child);} else if (target.getTop() &#61;&#61; monthCalendarHeight) {
setMonthMode(child);}}
}});}
}

到这里&#xff0c;自定义 Behavior 就算完成了。

效果

看一下最终的效果&#xff1a;

这种实现方式的优点是代码量少&#xff0c;用起来方便。使用了 MaterialCalendarView 并且没有修改它的源码&#xff0c;意味着支持它的所有功能。

希望通过本文&#xff0c;大家对 Behavior 有一个大概的了解。

项目地址&#xff1a;https://github.com/NanBox/NestedCalendar

Android开发资料&#43;面试架构资料 免费分享 点击链接 即可领取

《Android架构师必备学习资源免费领取&#xff08;架构视频&#43;面试专题文档&#43;学习笔记&#xff09;》


推荐阅读
  • 展开全部下面的代码是创建一个立方体Thisexamplescreatesanddisplaysasimplebox.#Thefirstlineloadstheinit_disp ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 带添加按钮的GridView,item的删除事件
    先上图片效果;gridView无数据时显示添加按钮,有数据时,第一格显示添加按钮,后面显示数据:布局文件:addr_manage.xml<?xmlve ... [详细]
  • 工作经验谈之-让百度地图API调用数据库内容 及详解
    这段时间,所在项目中要用到的一个模块,就是让数据库中的内容在百度地图上展现出来,如经纬度。主要实现以下几点功能:1.读取数据库中的经纬度值在百度上标注出来。2.点击标注弹出对应信息。3 ... [详细]
  • mysql-cluster集群sql节点高可用keepalived的故障处理过程
    本文描述了mysql-cluster集群sql节点高可用keepalived的故障处理过程,包括故障发生时间、故障描述、故障分析等内容。根据keepalived的日志分析,发现bogus VRRP packet received on eth0 !!!等错误信息,进而导致vip地址失效,使得mysql-cluster的api无法访问。针对这个问题,本文提供了相应的解决方案。 ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
  • 本文介绍了如何使用vue-awesome-swiper组件,包括在main.js中引入和使用swiper和swiperSlide组件,以及设置options和ref属性。同时还介绍了如何在模板中使用swiper和swiperSlide组件,并展示了如何通过循环渲染swipes数组中的数据,并使用picUrl属性显示图片。最后还介绍了如何添加分页器。 ... [详细]
  • 本文介绍了一款名为TimeSelector的Android日期时间选择器,采用了Material Design风格,可以在Android Studio中通过gradle添加依赖来使用,也可以在Eclipse中下载源码使用。文章详细介绍了TimeSelector的构造方法和参数说明,以及如何使用回调函数来处理选取时间后的操作。同时还提供了示例代码和可选的起始时间和结束时间设置。 ... [详细]
  • 微信小程序导航跟随的实现方法
    本文介绍了在微信小程序中实现导航跟随的方法。通过设置导航的position属性和绑定滚动事件,可以实现页面向下滚动到导航位置时,导航固定在页面最上方;页面向上滚动到导航位置时,导航恢复到原始位置;点击导航可以平滑跳转到相应位置。代码示例也给出了具体实现方法。 ... [详细]
  • 今日份分享:Flutter自定义之旋转木马
    今日份分享:Flutter自定义之旋转木马-先上图,带你回到童年时光:效果分析子布局按照圆形顺序放置且平分角度子布局旋转、支持手势滑动旋转、快速滑动抬手继续旋转、自动旋转支持X轴旋 ... [详细]
  • 1简介本文结合数字信号处理课程和Matlab程序设计课程的相关知识,给出了基于Matlab的音乐播放器的总体设计方案,介绍了播放器主要模块的功能,设计与实现方法.我们将该设 ... [详细]
  • SmartRefreshLayout自定义头部刷新和底部加载
    1.添加依赖implementation‘com.scwang.smartrefresh:SmartRefreshLayout:1.0.3’implementation‘com.s ... [详细]
  • python3 logging
    python3logginghttps:docs.python.org3.5librarylogging.html,先3.5是因为我当前的python版本是3.5之所 ... [详细]
  • 开发笔记:图像识别基于主成分分析算法实现人脸二维码识别
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了图像识别基于主成分分析算法实现人脸二维码识别相关的知识,希望对你有一定的参考价值。 ... [详细]
author-avatar
maniac0207
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有