前言:本篇是接前兩篇文章來實現日曆的嵌套滑動。日曆的嵌套滑動是分爲兩個部分來實現的,一部分是用戶滑動RecyclerView時處理RecyclerView的內容和自身的滑動,另一部分是處理Dayview跟隨RecyclerView的滑動。這樣分開處理,有利於減少干擾,簡化邏輯,而本文將實現實現第二部分,即用戶手指在DayView上滑動時的處理。
實現原理
1,當用戶未選中日曆時,手指滑動的距離交給RecyclerView來處理,RecyclerView的top滑動多少距離,則DayView滑動多少距離(同步移動)。
2,當用戶選中某一天時,DayView的最大滑動距離爲選中行數(第一行爲0)乘行高selectRow*child.getCeilHeight(),RecyclerView的滑動自然會覆蓋DayView的底部。因此重點是計算DayView的滑動距離。
DayView跟隨RecyclerView滑動
在Behavior中跟隨滑動很簡單,只需要實現layoutDependsOn和onDependentViewChanged方法即可。重點關注calucte方法,計算DayView應該滑動的距離。
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, DayView child, View dependency) {
return dependency instanceof RecyclerView; //讓DayView跟隨RecyclerView滑動
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, DayView child, View dependency) {
child.setTranslationY(-calucte(child, dependency));//讓DayView跟隨RecyclerView滑動
return true;
}
/**
* @param child
* @param dependency
* @return 返回DayView應該移動到目標位置的Y偏移值
*/
private int calucte( DayView child,View dependency){
int dy=0;
int recview_tranlasteY=(child.getMeasuredHeight()-dependency.getTop()); //recview移動的距離
int selectRow=child.getSelectedRow();
if (selectRow !=-1 ){//當用戶點擊了選中時
dy=recview_tranlasteY;
int maxY=selectRow*child.getCeilHeight();//DayView可移動的最大距離
if (recview_tranlasteY > maxY){
dy=maxY;
}
} else {//當用戶未點擊選中時 DayView與recview移動的距離一樣
dy=recview_tranlasteY;
}
return dy;
}
滑動事件的攔截
用戶手指在DayView滑動時該如何處理?上面只是讓DayView跟隨RecyclerView滑動,既用戶滑動RecyclerView時DayView能夠跟隨滑動,但用戶手指在DayView上滑動時,還無法滑動。這兒就需要攔截滑動事件,轉交給RecyclerView來處理,從上面的代碼可知,RecyclerView滑動那麼DayView自然就能滑動。
int mInterceptDownX,mInterceptDownY;
boolean isVerticalScroll;
public boolean onInterceptTouchEvent(CoordinatorLayout parent, DayView child, MotionEvent event) { //是CoordinatorLayout的onInterceptTouchEvent事件處理
boolean intercepted = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {//不攔截DOWN事件以便DayView能夠處理點擊事件
intercepted = false;// super.onInterceptTouchEvent(parent,child,event);
mInterceptDownX=(int)event.getX();
mInterceptDownY=(int)event.getY();
downY=mInterceptDownY;
lastTouchY=downY;
break;
}
case MotionEvent.ACTION_MOVE: {//攔截dayview的MOVE事件 外部攔截法,老套路
RecyclerView recView=(RecyclerView) parent.getChildAt(1);
if (mInterceptDownY < recView.getTop() && Math.abs(event.getY() - mInterceptDownY) > mTouchSlop ) {//當用戶點擊的範圍是dayview的範圍內,攔擊滑動事件交給onTouchEvent來處理
intercepted = true;
isVerticalScroll = true;
} else {
intercepted = false;// super.onInterceptTouchEvent(parent,child,event);
isVerticalScroll = false;
}
break;
}
case MotionEvent.ACTION_UP: {
if (isVerticalScroll){//如果dayview的MOVE事件攔截了,那麼UP事件也攔截
intercepted = true;
}else {
intercepted = false;// super.onInterceptTouchEvent(parent,child,event);
}
break;
}
default:
break;
}
return intercepted;
}
下面是將滑動事件攔截後轉交給RecyclerView來實現滑動。另外當用戶鬆開手指時,回彈的實現。
float lastTouchY;
float downY;
@Override
public boolean onTouchEvent(CoordinatorLayout parent, DayView child, MotionEvent ev) {//是CoordinatorLayout的onTouchEvent事件處理
boolean consume = false;
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN: //由於onInterceptTouchEvent不攔截DOWN事件 , 那麼onTouchEvent是獲取不到DOWN的事件
break;
case MotionEvent.ACTION_MOVE://此處實現用戶手指的滑動
float currentY=ev.getY();
float dy=currentY- lastTouchY;
lastTouchY =currentY;
int tranlasteY=caluteDeltaY( (RecyclerView) parent.getChildAt(1),(int) -dy ,child.getMeasuredHeight() ,child.getCeilHeight());
if (!ViewCompat.canScrollVertically(parent.getChildAt(1), -1)) { //判斷RecyclerView是否滑到頂部了
parent.getChildAt(1).offsetTopAndBottom(tranlasteY); //直接調用RecyclerView來滑動,那麼DayView就會跟隨滑動
consume=true;
}
break;
case MotionEvent.ACTION_UP: //此處實現用戶鬆開手指的回彈效果
float deletaY=ev.getY()- downY;
int offsetMaxY= child.getMeasuredHeight();
if (parent.getChildAt(1).getTop() > (offsetMaxY)/2 && Math.abs(deletaY)>mTouchSlop){ //向下滑動
scrollTo(parent,(RecyclerView)parent.getChildAt(1),child.getMeasuredHeight(),300);
}else if ( parent.getChildAt(1).getTop() < (offsetMaxY)/2 && Math.abs(deletaY) > mTouchSlop){//向上滑動
scrollTo(parent,(RecyclerView)parent.getChildAt(1),child.getCeilHeight(),300);
}
consume=true;
break;
}
return consume;//super.onTouchEvent(parent, child, ev);
}
/**
* 用於自動滾到目的位置,實現自動回彈功能
* @param parent
* @param child
* @param y
* @param duration
*/
private void scrollTo(final CoordinatorLayout parent, final RecyclerView child, final int y, int duration) {
final Scroller scroller = new Scroller(parent.getContext());
scroller.startScroll(0, child.getTop(), 0, y-child.getTop(), duration); //設置scroller的滾動偏移量
ViewCompat.postOnAnimation(child, new Runnable() {
@Override
public void run() {
//返回值爲boolean,true說明滾動尚未完成,false說明滾動已經完成。
// 這是一個很重要的方法,通常放在View.computeScroll()中,用來判斷是否滾動是否結束。
if (scroller.computeScrollOffset()) {
int delta = scroller.getCurrY() - child.getTop();
child.offsetTopAndBottom(delta);
parent.dispatchDependentViewsChanged(child);
ViewCompat.postOnAnimation(child, this);
}
}
});
}
/**
* @param child
* @param dy
* @param offsetMaxY
* @param offsetMinY
* @return 主要計算用戶滑動是否滑出邊界的情況
*/
private int caluteDeltaY(RecyclerView child, int dy ,int offsetMaxY ,int offsetMinY){
int deletaY=0;
int offsetY = child.getTop();
int targetY=child.getTop()-dy;
if (targetY < offsetMinY){//滑出上邊界了
deletaY = offsetY-offsetMinY;
}else if (targetY > offsetMaxY){//滑出下邊界了
deletaY = offsetY -offsetMaxY;
}else {
deletaY=dy;
}
return -deletaY;
}
總結
通過以上三篇文章可以看出,一個看似複雜的嵌套滑動日曆,在經過詳細的的功能拆解後,其實現都是相對簡單的,關鍵在於拆解功能的能力,這就需要長期的積累了,路漫漫其修遠兮,吾將上下而求索。