本文已受权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发git
老规矩先贴效果图github
github地址,以为有帮助的能够给个 star 呗bash
添加依赖ide
compile 'com.github.idic779:monthweekmaterialcalendarview:1.7'布局
具体如何使用看这里ui
能够控制是否容许左右滑动,上下滑动,切换年月this
流畅的上下周月模式切换spa
自定义日历样式.net
基于material-calendarview 这个库实现,能够根据需求定制效果
以前开发任务中有涉及到年月日日历的切换效果,因为是须要联动,想到的方向大概有3种,要么经过处理view
的touch
事件,要么是经过自定义behavior
去实现,要么是经过ViewDragHelper
这个神器去实现,网上比较多的是经过自定义behavior
去实现,本文使用的是第三种方法,实现的是一个可高度定制自由切换的周月日历视图,提供一种思路去实现页面联动效果。 ##准备
因为重点实现的是年月切换的效果,原本想着说能够本身写一个日历组件而后再加上ViewDragHelper
,应该能够实现周月联动的效果吧?后面想了想,重点在切换那就干脆直接找个开源库稳定性好点的日历组件,因此用github.com/prolificint…快4000start的库吧, ViewDragHelper
,做为一个神器能够作不少的事情,官方的DrawerLayout
,BottomSheetBehavior
用他来实现,为何用它?对于拖动某个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()
里面设置相关的回调处理,例如日期选中或者月份切换的回调等。
好的大工告成。