前言
这个日历是一个仿MIUI
交互的日历,尽可能实现MIUI
日历的交互设计,加入了一些自定义属性,如设置默认视图,设置一周的第一天是周日还是周一等。这个日历是在之前我写的那个日历基础上改的,里面的关于绘制的部分和之前是一样的,这篇文章就不再说了,这次主要说一下怎么实现miui
日历的滑动效果。
效果图
项目地址
http://www.apkbus.com/thread-599823-1-1.html
日历实现思路
1、视图切换。周日历显示的位置是不变的,一直都在布局的顶部,可以固定在顶部,NCalendar
类主要控制月日历MonthCalendar
和NestedScrollingChild
,随着滑动位置的变化控制周日历的显示和隐藏。
2、滑动的处理。计算不同选中日期月日历MonthCalendar
和NestedScrollingChild
的子类所需要滑动的距离,使用View
的offsetTopAndBottom(int offset)
方法完成滑动。
具体实现
1、初始化NCalendar
类
在NCalendar
类的构造方法中,首先new
了一个月日历MonthCalendar
和一个周日历WeekCalendar
,并确定这两个日历的高度,月日历的高度可以通过自定义属性设置,默认为300dp
,周日历则为月日历的五分之一,然后把月日历MonthCalendar
和周日历WeekCalendar
通过addView
方法添加到NCalendar
中,在onLayout
中排列各自的位置。代码
public NCalendar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); monthCalendar = new MonthCalendar(context, attrs); weekCalendar = new WeekCalendar(context, attrs); weekHeigh = monthHeigh / 5; monthCalendar.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, monthHeigh)); weekCalendar.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, weekHeigh)); addView(monthCalendar); addView(weekCalendar); post(new Runnable() { @Override public void run() { weekCalendar.setVisibility(STATE == MONTH ? INVISIBLE : VISIBLE); } }); }
实际项目中,构造方法中还有设置默认视图,初始化动画等,这里只贴出来关键代码。
2、onNestedPreScroll
NCalendar
实现了NestedScrollingParent
,主要的交互都在这两个方法中完成,onNestedPreScroll
和onStopNestedScroll
,前一个是嵌套滑动时调用,后一个是停止滑动的回调。我们需要在onNestedPreScroll
中完成上滑和下滑时,改变月日历MonthCalendar
和NestedScrollingChild
的位置,在onStopNestedScroll
中完成手指离开后自动滑动。miui
日历的上滑操作是先让月日历滑动到选中的选中的那一行,再向上移动NestedScrollingChild
,下滑时也是先移动月日历,再移动NestedScrollingChild
。NClendar
在处理这个滑动时分了四种情况:
月日历和NestedScrollingChild同时上滑。 月日历上滑到一定位置后,NestedScrollingChild单独上滑。 月日历和NestedScrollingChild同时下滑。 月日历下滑到一定位置后,NestedScrollingChild单独下滑。
这四种情况判断的条件就是月日历和NestedScrollingChild
距离顶部的位置。NestedScrollingChild
距顶部距离的判断比较简单,月视图时,距离就是月日历的高度,周视图时就是周日历的高度。月日历距顶部的距离稍微复杂一下,需要先计算出来月日历总共需要滑动的距离monthCalendarOffset
,再通过offsetTopAndBottom
移动,移动的时候,不断获取月日历的getTop()
,当getTop()
达到总的偏移量时,就说明月日历已经移动到指定位置,接下来就是NestedScrollingChild
单独滑动。上滑下滑都是这个逻辑。代码:
@Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { int monthTop = monthCalendar.getTop(); int nestedScrollingChildTop = nestedScrollingChild.getTop(); monthCalendarOffset = getMonthCalendarOffset(); //4种情况 if (dy > 0 && Math.abs(monthTop) < monthCalendarOffset) { //月日历和nestedScrollingChild同时上滑 int offset = getOffset(dy, monthCalendarOffset - Math.abs(monthTop)); monthCalendar.offsetTopAndBottom(-offset); nestedScrollingChild.offsetTopAndBottom(-offset); consumed[1] = dy; } else if (dy > 0 && nestedScrollingChildTop > weekHeigh) { //月日历滑动到位置后,nestedScrollingChild继续上滑,覆盖一部分月日历 int offset = getOffset(dy, nestedScrollingChildTop - weekHeigh); nestedScrollingChild.offsetTopAndBottom(-offset); consumed[1] = dy; } else if (dy < 0 && monthTop != 0 && !ViewCompat.canScrollVertically(target, -1)) { //月日历和nestedScrollingChild下滑 int offset = getOffset(Math.abs(dy), Math.abs(monthTop)); monthCalendar.offsetTopAndBottom(offset); nestedScrollingChild.offsetTopAndBottom(offset); consumed[1] = dy; } else if (dy < 0 && monthTop == 0 && nestedScrollingChildTop != monthHeigh && !ViewCompat.canScrollVertically(target, -1)) { //月日历滑动到位置后,nestedScrollingChild继续下滑 int offset = getOffset(Math.abs(dy), monthHeigh - nestedScrollingChildTop); nestedScrollingChild.offsetTopAndBottom(offset); consumed[1] = dy; } //nestedScrollingChild滑动到周位置后,标记状态,同时周日显示 if (nestedScrollingChildTop == weekHeigh) { STATE = WEEK; weekCalendar.setVisibility(VISIBLE); } //周状态,下滑显示月日历,把周日历隐掉 if (STATE == WEEK && dy < 0 && !ViewCompat.canScrollVertically(target, -1)) { weekCalendar.setVisibility(INVISIBLE); } //彻底滑到月日历,标记状态 if (nestedScrollingChildTop == monthHeigh) { STATE = MONTH; } }
根据需求,需要判断NestedScrollingChild
的条目已经不能再滑动时才移动NestedScrollingChild
本身。在滑动过程中,要标记当前视图的状态 MONTH
或者WEEK
。其中计算monthCalendarOffset
的方法:
//月日历需要滑动的距离, private int getMonthCalendarOffset() { NMonthView currectMonthView = monthCalendar.getCurrectMonthView(); //该月有几行 int rowNum = currectMonthView.getRowNum(); //现在选中的是第几行 int selectRowIndex = currectMonthView.getSelectRowIndex(); //month需要移动selectRowIndex*h/rowNum ,计算时依每个行高的中点计算 int monthCalendarOffset = selectRowIndex * currectMonthView.getDrawHeight() / rowNum; return monthCalendarOffset; }
计算方法是,得到当前月的View
NMonthView
,再通过该月的行数(5或6)、被选中的日期在哪一行以及NMonthView
的绘制高度算出月日历需要移动的距离。月日历的绘制高度和月日历的高度不是一个数值,因为当月份有6行时,公历日期绘制在一行的中间位置,下面的农历就没有太多的地方绘制,在最后一行的农历就会和月日历底部非常接而影响美观,为了避免这种情况,日历View
绘制的时候,把绘制高度比日历高度小了一点,这里需要计算的移动量是由绘制区域的行高决定。
3、onStopNestedScroll
这个方法处理自动滑动的问题。在滑动过程中如果松手,日历要自动回到对应的位置,对应的位置就是说,滑动的距离小时,还回到原来的位置,滑动的距离大时,回到相反的位置。这里的动画用的是ValueAnimator
,在初始化NCalendar
时,new
了两个ValueAnimator
对象,在onStopNestedScroll
回调时,分别给他们的起始值和结束值,再通过动画中得到的getAnimatedValue
值,计算偏移量,执行offsetTopAndBottom
方法,完成动画。
@Override public void onStopNestedScroll(View target) { //停止滑动的时候,距顶部的距离 int monthCalendarTop = monthCalendar.getTop(); int nestedScrollingChildTop = nestedScrollingChild.getTop(); if (monthCalendarTop == 0 && nestedScrollingChildTop == monthHeigh) { return; } if (monthCalendarTop == -monthCalendarOffset && nestedScrollingChildTop == weekHeigh) { return; } if (STATE == MONTH) { //nestedScrollingChild移动的超过周高度时才会滑动到周 if (monthHeigh - nestedScrollingChildTop < weekHeigh) { autoScroll(monthCalendarTop, 0, nestedScrollingChildTop, monthHeigh); } else { autoScroll(monthCalendarTop, -monthCalendarOffset, nestedScrollingChildTop, weekHeigh); } } else { //nestedScrollingChild移动的超过周高度时才会滑动到月 if (nestedScrollingChildTop < weekHeigh * 2) { autoScroll(monthCalendarTop, -monthCalendarOffset, nestedScrollingChildTop, weekHeigh); } else { autoScroll(monthCalendarTop, 0, nestedScrollingChildTop, monthHeigh); } } }
autoScroll方法:
//自动滑动 private void autoScroll(int startMonth, int endMonth, int startChild, int endChild) { monthValueAnimator.setIntValues(startMonth, endMonth); monthValueAnimator.setDuration(duration); monthValueAnimator.start(); nestedScrollingChildValueAnimator.setIntValues(startChild, endChild); nestedScrollingChildValueAnimator.setDuration(duration); nestedScrollingChildValueAnimator.start(); }
ValueAnimator
动画的回调:
@Override public void onAnimationUpdate(ValueAnimator animation) { if (animation == monthValueAnimator) { int animatedValue = (int) animation.getAnimatedValue(); int top = monthCalendar.getTop(); int i = animatedValue - top; monthCalendar.offsetTopAndBottom(i); } if (animation == nestedScrollingChildValueAnimator) { int animatedValue = (int) animation.getAnimatedValue(); int top = nestedScrollingChild.getTop(); int i = animatedValue - top; nestedScrollingChild.offsetTopAndBottom(i); } }
到此,交互的部分就结束了。
其他问题
写完 以上这些,这个日历算是基本完工,但是还是有不少bug的,其他的问题,就在写的过程中解决的一些bug。
1、滑动过快的问题
快速滑动nestedScrollingChild
时,有时会出现不友好的情况,这有两个地方做了限制。
一个是NestedScrollingParent
中的onNestedPreFling
方法:
@Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { //防止快速滑动 int nestedScrollingChildTop = nestedScrollingChild.getTop(); if (nestedScrollingChildTop > weekHeigh) { return true; } return false; }
上面的方法主要限制nestedScrollingChild
的快速滑动。还有一个地方是月日历的快速移动,是滑动到边界的问题:
private int getOffset(int offset, int maxOffset) { if (offset > maxOffset) { return maxOffset; } return offset; }
这个方法是获取偏移量的时候,如果得到的数值大于需要的最大值,则返回最大值,防止出现view
越界的情况。
2、翻页闪烁的问题 onLayout
这个问题,是当滑动到相应位置后,左右翻页月日历,会出现月日历返回到原来移动之前的位置上,造成闪烁,并且此时位置也乱了。这时就需要在onLayout
重新确定月日历和nestedScrollingChild
的位置,需要在每次操作之后执行requestLayout()
方法:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // super.onLayout(changed, l, t, r, b); if (STATE == MONTH) { monthCalendarTop = monthCalendar.getTop(); childViewTop = nestedScrollingChild.getTop() == 0 ? monthHeigh : nestedScrollingChild.getTop(); } else { monthCalendarTop = -getMonthCalendarOffset(); childViewTop = nestedScrollingChild.getTop() == 0 ? weekHeigh : nestedScrollingChild.getTop(); } monthCalendar.layout(0, monthCalendarTop, r, monthHeigh + monthCalendarTop); ViewGroup.LayoutParams layoutParams = nestedScrollingChild.getLayoutParams(); nestedScrollingChild.layout(0, childViewTop, r, layoutParams.height + childViewTop); }
3、滑动到周日历底部空白的问题 onMeasure
onMeasure
的问题,当日历滑动到周日历的之后,NestedScrollingChild
下方会出现空白,这个空白是由于NestedScrollingChild
上移造成的,因为NestedScrollingChild
高度一定,上移以后,下面没有东西了自然就会留空。我们可以把NestedScrollingChild
的高度变高,这样上滑之后,之前没有显示的那部分就会显示出来,就不会有空白了。观察之后会发现,NestedScrollingChild
移动到周日历的位置后,整个View
上面是周日历,下面是NestedScrollingChild
,所以我们可以把NestedScrollingChild
的高度变成整个View
的高度和周日历高度之差,这样就可以了:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); ViewGroup.LayoutParams layoutParams = nestedScrollingChild.getLayoutParams(); layoutParams.height = getMeasuredHeight() - weekHeigh; }
至此,这个日历算是完成了。