纵享丝滑滑动切换的周月日历,水滴效果,可随意自定义日历样式,仿小米日历(ViewDragHelper实现)

本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发

老规矩先贴效果图

样式.gif

水滴效果.gif

p1.jpg

p2.jpg

github地址,觉得有帮助的可以给个 star 呗

https://github.com/idic779/monthweekmaterialcalendarview

添加依赖

compile 'com.github.idic779:monthweekmaterialcalendarview:1.7'

具体如何使用看这里

这个库可以做什么?

  • 可以控制是否允许左右滑动,上下滑动,切换年月

  • 流畅的上下周月模式切换

  • 自定义日历样式

  • 基于material-calendarview 这个库实现,可以根据需求定制效果

之前开发任务中有涉及到年月日日历的切换效果,由于是需要联动,想到的方向大概有3种,要么通过处理viewtouch事件,要么是通过自定义behavior去实现,要么是通过ViewDragHelper这个神器去实现,网上比较多的是通过自定义behavior去实现,本文使用的是第三种方法,实现的是一个可高度定制自由切换的周月日历视图,提供一种思路去实现页面联动效果。

准备

由于重点实现的是年月切换的效果,本来想着说可以自己写一个日历组件然后再加上ViewDragHelper,应该可以实现周月联动的效果吧?后面想了想,重点在切换那就干脆直接找个开源库稳定性好点的日历组件,所以用https://github.com/prolificinteractive/material-calendarview快4000start的库吧,
ViewDragHelper,作为一个神器可以做很多的事情,官方的DrawerLayoutBottomSheetBehavior用他来实现,为什么用它?对于拖动某个View,如果是自己去重写touch事件的,计算滑动距离再去移动View会需要处理比较多繁琐的代码去实现。如果我们用ViewDragHelper的话能很轻易的实现这样的效果。
简单的介绍下ViewDragHelper

   ViewDragHelper helper= ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return true;
        }
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx)
        {
            return left;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy)
        {
            return top;
        }
        @Override
        public int getViewHorizontalDragRange(View child) {
            return super.getViewHorizontalDragRange(child);
        }

        @Override
        public int getViewVerticalDragRange(View child) {
            return super.getViewVerticalDragRange(child);
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
        }
    });
  • tryCaptureView():如果返回true,则说明可以捕获该view,我们可以在这里设置捕获的条件
  • clampViewPositionHorizontal ()``clampViewPositionVertical()
    分别对child水平和竖直方向移动的边界进行控制,例如限制周月移动的距离可以在这里做处理
  • onViewPositionChanged() : 当child的位置发生移动时候会回调这个方法
  • onViewReleased():手指释放时候的回调
  • getViewHorizontalDragRange()``getViewVerticalDragRange():返回child横向或者纵向移动的范围,大于0才能捕获。

更多的可以参考鸿洋的Android ViewDragHelper完全解析 自定义ViewGroup神器

如何实现

既然选择ViewDragHelper要实现周月联动呢,我们来理一理要实现的效果,在月视图的时候,能够把下面的recyclerView上移拖到到周视图的高度,上移过程如果超过一定距离就默认滚动到周视图。
在周视图的的时候又能把recyclerView下移拖动到月视图的高度位置,下移过程如果超过一定距离就默认滚动到月视图。

整体分析

整个页面是由顶部的周名字的View、周模式的MaterialCalendarView、月模式的MaterialCalendarView和最下面的recyclerView组成
需要注意的是MaterialCalendarView 这个库原来是有周名字还有顶部显示日期的,
需要注意的是这里稍微做了下修改把这些给隐藏掉了,具体可以看MaterialCalendarView.setTopbarVisible()。并且做了下修改增加了获得单行的高度方法MaterialCalendarView.getItemHeight() ,即为周模式时显示的高度。

具体实现

  • 拖动前处理
    整个页面只有recyclerView ,月模式下如果向上拖动时候如果recyclerView不是滚动到了顶部的话那么就不允许拖动,相关代码
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return !mDragHelper.continueSettling(true)
                    &&child == mRecyclerView && !animatStart
                    && isAtTop(mRecyclerView) &&                              
                          !ViewCompat.canScrollVertically(mRecyclerView, -1);
        }

  • 限制recyclerView移动的高度在周模式和月模式之间
       @Override
       public int clampViewPositionVertical(View child, int top, int dy) {
           //决定竖直方向上能移动的距离为 finalWeekModeHeight到finalMonthModeHeight
           int topBound = finalWeekModeHeight;
           int bottomBound = finalMonthModeHeight;
           int newTop = Math.min(Math.max(top, topBound), bottomBound);
           return newTop;
       }
  • onMeasure获得初始的一些数据值,包括周模式的高度,月模式的高度,最大移动的距离,单行的高度
 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     calendarItemHight = mCalendarViewMonth.getItemHeight();
     calendarWeekHight = calendarItemHight;
     if (defaultStopHeight == 0) {
         defaultStopHeight = getCurrentItemPosition(CalendarDay.today()) * calendarItemHight;
     }
     calendarMonthHight = mCalendarViewMonth.getMeasuredHeight();
     weekViewHight = mTopWeekView.getMeasuredHeight();
     finalMonthModeHeight = weekViewHight + calendarMonthHight;
     finalWeekModeHeight = calendarItemHight + weekViewHight;
     maxOffset = calendarMonthHight - calendarItemHight;
 }
  • 然后在onlayout()把布局里的View绘制到对应的位置上面

  • 最大移动的距离defaultStopHeight在选中日期时候就会通过 getCurrentItemPosition()计算出它点击所在的行数再调用setStopItemPosition()就可以得到要停止下来的高度,

  • 接下来说下最关键的地方
    既然是周月联动我们发现在拖动recyclerView视图的时候我们会不停回调onViewPositionChanged()这个方法,我们在这个方法里面就可以根据recyclerView移动的距离来移动对应的月视图,

//滑动处理
       private void HandlerOffset(View changedView, int left, int top, int dx, int dy) {
           //获取日历相对手指移动的相对距离 dy向上移动小于0
           transY = transY + dy;
           if (transY > 0) {
               transY = 0;
           }
           if (transY < -calendarMonthHight - calendarItemHight) {
               transY = -calendarMonthHight - calendarItemHight;
           }

           float abstransY = Math.abs(transY);
           if (dy < 0) {
               //如果上滑动,并且滑向动的绝对值距离在超过calendarHight-defaultStopHeight
               // 并且小于可以滑动的距离calendarHight-calendarItemHight之间的话
               if (abstransY >= (calendarMonthHight - defaultStopHeight) && abstransY < calendarMonthHight - calendarItemHight) {
                   if (!animatStart) {
                       mCalendarViewMonth.setTranslationY(getOffset((int) mCalendarViewMonth.getTranslationY() + dy, calendarItemHight - defaultStopHeight));
                   }
               }
           }
           if (dy > 0) {
               if (abstransY < maxOffset
                       && currentMode.equals(Mode.WEEK)) {
                   mCalendarViewWeek.setVisibility(INVISIBLE);
               }
               if (abstransY < maxOffset) {
                   mCalendarViewMonth.setTranslationY(getOffset((int) mCalendarViewMonth.getTranslationY() + dy, 0));
               }

           }

       }

月视图的移动我们是通过setTranslationY来移动的,为了防止滑动时候过快通过getOffset()限制一下它滑动的最大距离。

  • 在松开手指的时候我们在onViewReleased()做相关状态的改变,如果滑动的距离超过一定的值就把当前视图置为月模式还是周模式
       @Override
          public void onViewReleased(View releasedChild, float xvel, float yvel) {
              int moveY = finalMonthModeHeight - mRecyclerView.getTop();
              //周模式距离滑动为一行的高度,超过就滑动到周位置
              int weekdistance = calendarItemHight;
              //最大滑动距离
              int maxDistance = calendarMonthHight;
              if (currentMode == Mode.MONTH) {
                  //如果滑动距离超过当前选中项和最大滑动距离之间的距离
                  if (moveY > weekdistance && moveY < maxDistance) {
                      //变为周模式
                      setMode(Mode.WEEK);
                  } else if (moveY <= weekdistance) {
                      //变为月模式
                      setMode(Mode.MONTH);
                  }
              } else {
                  //周模式下距离顶部选中日期的距离小于最大滑动距离-10的话就让它变为月模式
                  if (moveY > maxOffset - 10) {
                      //变为周模式
                      setMode(Mode.WEEK);
                  } else if (moveY <= maxOffset - 10) {
                      //变为月模式
                      setMode(Mode.MONTH);
                  }
              }
          }
    

需要注意的是在onInterceptTouchEvent()如果是月模式并且可以拖动的时候,
底部的recyclerView是不允许滑动的

if (currentMode == Mode.MONTH&& canDrag) {
               setRecyclerViewCanScroll(false);
}

还可以怎么用

接下来说下你可以怎么去定制?如果你想替换项目中的月和周视图的话,不想用Material-calendarview ,很简单,只需要你自己的周月视图必须有一个方法获得单行日历的高度(例如我的库中的MaterialCalendarView.getItemHeight() ),然后把这个月视图和周视图,分别在MonthWeekMaterialCalendarView里面按照顺序放到对应位置即可。然后再setListener()里面设置相关的回调处理,例如日期选中或者月份切换的回调等。

好的大工告成。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容