这一次拆解的是今日头条的关注页面:点击关注的头像会弹出一个文章列表。在边界拖拽会出现关闭提示。此次同时实现了Android端和IOS端的效果。git
效果以下图: github
弹出来的页面能够左右切换,每一个页面是单独的列表,能上下滑动,因此这里直接用viewPager+recycelrView实现。 当viewPager不能左右滑动的时候,移动整个viewPager,出现文字提示,当滑动距离超过阈值时,文字改变。 当手指松开时,若滑动距离未到达阈值,回弹;不然结束页面。 一样,当recyclerView在顶部不能滑动时,移动recyclerView,出现提示,后续跟viewPager一致故再也不赘述。canvas
这里的回弹我自定义了一个回弹布局,下面介绍一下回弹布局的几个重要方法: onInterceptTouchEvent()bash
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
//记录坐标
break;
case MotionEvent.ACTION_MOVE:
int difX = (int) (ev.getX() - mDownX);
int difY = (int) (ev.getY() - mDownY);
if (orientation == LinearLayout.HORIZONTAL) {
.....
if (水平滑动) {
if (!innerView.canScrollHorizontally(-1) && difX > 0) {
//右拉到边界
return true;
}
if (!innerView.canScrollHorizontally(1) && difX < 0) {
//左拉到边界
return true;
}
}
} else {
......
if (竖直滑动) {
if (!innerView.canScrollVertically(-1) && difY > 0) {
//下拉到边界
return true;
}
if (!innerView.canScrollVertically(1) && difY < 0) {
//上拉到边界
return true;
}
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
......重置变量
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
复制代码
当控件方向为横向且滑动为水平滑动时,检测innerView可否在该方向上滑动;若不能,则拦截事件,交给自身处理(纵向同理)。 拦截事件后,在**onTouchEvent()**进行处理,实现移动和回弹。ide
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
if (orientation == LinearLayout.HORIZONTAL) {
int difX = (int) ((event.getX() - mDownX) / resistance);
boolean isRebound = false;
if (!innerView.canScrollHorizontally(-1) && difX > 0) {
//右拉到边界
isRebound = true;
} else if (!innerView.canScrollHorizontally(1) && difX < 0) {
//左拉到边界
isRebound = true;
}
if (isRebound) {
//移动和回调
return true;
}
} else {
int difY = (int) ((event.getY() - mDownY) / resistance);
boolean isRebound = false;
if (!innerView.canScrollVertically(-1) && difY > 0) {
//下拉到边界
isRebound = true;
} else if (!innerView.canScrollVertically(1) && difY < 0) {
//上拉到边界
isRebound = true;
}
if (isRebound) {
//移动和回调
return true;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (orientation == LinearLayout.HORIZONTAL) {
int difX = (int) innerView.getTranslationX();
if (difX != 0) {
if (Math.abs(difX) <= resetDistance || isNeedReset) {
innerView.animate().translationX(0).setDuration(mDuration).setInterpolator(mInterpolator);
}
//回调
}
} else {
int difY = (int) innerView.getTranslationY();
if (difY != 0) {
if (Math.abs(difY) <= resetDistance || isNeedReset) {
innerView.animate().translationY(0).setDuration(mDuration).setInterpolator(mInterpolator);
}
//回调
}
}
break;
default:
break;
}
return super.onTouchEvent(event);
}
复制代码
MOVE事件 利用**setTranslationX()和setTranslationY()**改变innerView的位置,同时将滑动距离和方向经过接口回调到外面。布局
UP事件 判断滑动距离是否小于阈值,小于则执行回弹动画;同时回调到外面。 以上就是回弹布局的简单实现,主要是对滑动事件进行拦截处理,若是不清楚事件传递机制能够到这里查看。 布局有3个自定义属性动画
<declare-styleable name="ReBoundLayout">
<attr name="reBoundOrientation" format="enum">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
<attr name="resistance" format="float" />
<attr name="reBoundDuration" format="integer" />
</declare-styleable>
复制代码
分别是:回弹方向、阻力系数、回弹时间,剩余属性能够调用**set()**方法修改。ui
好了,如今回弹实现了,接下来就是将文字提示加上,结束动画加上。这里有一点须要注意的是:demo中使用的是reBoundLayout+viewPager+fragment(reBoundLayout+recyclerView)的结构实现的。而文字是跟viewPager同一层级的,因此须要把fragment的回调回调到activity里(也能够getActivity()获取对应的文字控件),详见代码。 如下是回调的伪代码:spa
@Override
public void onDistanceChange(int distance, int direction) {
switch (direction) {
case DIRECTION_LEFT:
if (distance > showTipDistance) {
//文字改变,移动
} else {
rightTip.setVisibility(View.GONE);
}
break;
case DIRECTION_RIGHT:
if (distance > showTipDistance) {
//文字改变,移动
} else {
leftTip.setVisibility(View.GONE);
}
break;
case DIRECTION_UP:
break;
case DIRECTION_DOWN:
//fragment的回调会走到这里
if (distance > showTipDistance) {
//文字改变,移动
} else {
topTip.setVisibility(View.GONE);
}
break;
default:
break;
}
}
@Override
public void onFingerUp(int distance, int direction) {
switch (direction) {
case DIRECTION_LEFT:
if (distance > mResetDistance) {
//结束页面
} else {
//文字重置
}
break;
case DIRECTION_RIGHT:
if (distance > mResetDistance) {
//结束页面
} else {
//文字重置
}
break;
case DIRECTION_DOWN:
if (distance > mResetDistance) {
//结束页面
} else {
//文字重置
}
break;
default:
break;
}
}
复制代码
效果以下: .net
PS:若是想圆心跟随手指移动,须要增长如下计算:圆最大半径、圆可移动距离与半径变化关系
关键变量:
事件拦截跟ReBoundLayout一致,因此不赘述,主要看看滑动事件的处理
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
int difX = (int) ((event.getX() - mDownX) / resistance);
int difY = (int) ((event.getY() - mDownY) / resistance);
if (orientation == LinearLayout.HORIZONTAL) {
boolean needDrag = false;
if (!innerView.canScrollHorizontally(-1) && difX > 0) {
//右啦到边界
needDrag = true;
} else if (!innerView.canScrollHorizontally(1) && difX < 0) {
//左拉到边界
needDrag = true;
}
if (needDrag) {
//半径计算
mTranslationX = difX;
mTranslationY = difY;
invalidate();
//回调
return true;
}
} else {
if (!innerView.canScrollVertically(-1) && difY > 0) {
//下拉到边界
//回调
return true;
} else if (!innerView.canScrollVertically(1) && difY < 0) {
//上啦到边界
innerView.setTranslationY(difY);
return true;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (orientation == LinearLayout.HORIZONTAL) {
//水平
if (Math.abs(mTranslationX) >= resetDistance) {
//回调
} else {
//重置状态
}
} else {
//竖直
if (innerView.getTranslationY() < 0) {
innerView.animate().setDuration(mDuration).translationY(0).setInterpolator(mInterpolator);
} else {
//回调
}
}
break;
default:
break;
}
return super.onTouchEvent(event);
}
复制代码
这里跟ReBoundLayout有如下几点区别:
PS:DragZoomLayout必定要设置背景,否则调用invalidate()会无效;上下滑动的mTranslationX、mTranslationY一直都是0(由于下滑咱们已经回调给最层的DragZoomLayout),因此在ACTION_UP、ACTION_CANCEL事件,竖直方向回调时是使用当前事件的x、y跟点击的x、y相减的值去回调。
@Override
protected void onDraw(Canvas canvas) {
if (Math.abs(mTranslationX) > mLargeX) {
mTranslationX = mTranslationX > 0 ? mLargeX : -mLargeX;
}
if (Math.abs(mTranslationY) > mLargeY) {
mTranslationY = mTranslationY > 0 ? mLargeY : -mLargeY;
}
canvas.translate(mTranslationX, mTranslationY);
mPath.reset();
mPath.addCircle(mPoint.x, mPoint.y, mRadius, Path.Direction.CCW);
canvas.clipPath(mPath);
super.onDraw(canvas);
}
复制代码
进行了一些位置和半径的限制。 布局完成,接下来处理页面间的接口回调及结束动画
动画的计算有一点点麻烦,数学很差的同窗请多看几遍,仍是不懂的趁着过年回高中找数学老师要回学费吧。
这是年前最后一篇博客了,今年立的flag好像都没有实现,跟大佬的差距仍是那么大,Bug仔仍需努力呀。