这一次拆解的是今日头条的关注页面:点击关注的头像会弹出一个文章列表。在边界拖拽会出现关闭提示。这次同时实现了Android端和IOS端的效果。

先讲解Android端的实现吧,毕竟我是个Android开发仔呀

效果如下图:

Android端

弹出来的页面可以左右切换,每个页面是单独的列表,能上下滑动,所以这里直接用viewPager+recycelrView实现。

当viewPager不能左右滑动的时候,移动整个viewPager,出现文字提示,当滑动距离超过阈值时,文字改变。

当手指松开时,若滑动距离未到达阈值,回弹;否则结束页面。

同样,当recyclerView在顶部不能滑动时,移动recyclerView,出现提示,后续跟viewPager一致故不再赘述。

ReBoundLayout

这里的回弹我自定义了一个回弹布局,下面介绍一下回弹布局的几个重要方法:

onInterceptTouchEvent()

@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()进行处理,实现移动和回弹。

@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个自定义属性

分别是:回弹方向、阻力系数、回弹时间,剩余属性可以调用set()方法修改。

好了,现在回弹实现了,接下来就是将文字提示加上,结束动画加上。这里有一点需要注意的是:demo中使用的是reBoundLayout+viewPager+fragment(reBoundLayout+recyclerView)的结构实现的。而文字是跟viewPager同一层级的,所以需要把fragment的回调回调到activity里(也可以getActivity()获取对应的文字控件),详见代码。

以下是回调的伪代码:

@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;

}

}

大功告成,Android端的效果比较简单,实现起来也比较容易。

IOS端效果复杂一丢丢,大家留心看。

效果如下:

IOS端

当页面不能拖动时(右滑、左滑、下滑),view的位置开始改变,并且整个页面会缩小成一个圆;当松手时距离大于阈值,view缩小为一个圆并平移到进入的那个圆位置,结束当前页面;否则回弹(demo中只给出一个圆,若需实现头条的效果,只需更改对应Point点得坐标即可)。

同样,自定义一个布局进行滑动事件的处理,至于整个页面的缩小变圆,这里通过裁剪画布的方式去实现(圆心固定在屏幕中央),也可以通过别的方法(Xfermode)去实现同样的效果,有兴趣的朋友自行探索。

PS:如果想圆心跟随手指移动,需要增加以下计算:圆最大半径、圆可移动距离与半径变化关系

DragZoomLayout

关键变量:

mMinRadius 圆最小半径

mMaxRadius 圆最大半径

mRadius 当前半径

mTranslationX 当前X移动距离

mTranslationY 当前Y移动距离

事件拦截跟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有以下几点区别:

通过裁剪画布的方式达到view缩小成圆的效果

通过移动画布达到移动view的效果(setTranslation会触发view的重绘,同时改变x跟y,会调用2次,而修改画布大小又需要重绘,调用次数太多,因此不使用该方式)

下滑跟左右滑动一样,缩小、移动的都是最外层的DragZoomLayout(这样视觉效果最好,而且能统一处理);上滑只做移动和回弹。

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);

}

进行了一些位置和半径的限制。

布局完成,接下来处理页面间的接口回调及结束动画

动画的计算有一点点麻烦,数学不好的同学请多看几遍,还是不懂的趁着过年回高中找数学老师要回学费吧。

先来看没有移动画布的情况:

启动页面时,通过getLocationOnScreen()获取进入时的坐标,退出时的坐标通过最外层的dragLayout的坐标加上宽高的一半,再减去圆的最小半径得到,最后通过这2个差值进行平移。

那么有平移并且半径未到最小的情况也可以通过这种方式计算:

我们已经有一个translationX了,那可以计算出目标的translationX,然后使用ValueAnimator不断去改变它进行重绘,得到一个平移效果(translationY同理)。那这个值要怎么得到呢?上面已经说了怎么计算了,没看懂的再看一遍。看几遍还是不懂的,回去找老师要学费吧。

至于进入动画原理相同,只是反过来执行罢了,这里不再赘述,详见代码。

有更好实现方式的欢迎下方留言讨论,有bug或者疑问的也可以留言,有空会回复的。

由于篇幅关系,一些细小的地方没有提及,有兴趣的朋友可以自行查看。

最后奉上源码;

这是年前最后一篇博客了,今年立的flag好像都没有实现,跟大佬的差距还是那么大,Bug仔仍需努力呀。

9102冲鸭

android 拖拽布局,Android拖拽、回弹布局相关推荐

  1. Android拖拽、回弹布局

    这一次拆解的是今日头条的关注页面:点击关注的头像会弹出一个文章列表.在边界拖拽会出现关闭提示.这次同时实现了Android端和IOS端的效果. 先讲解Android端的实现吧,毕竟我是个Android ...

  2. Android表格拖拽排序,Android 拖拽排序控件 DragGridView

    Android 拖拽排序控件 DragGridView Android 开发中,我们经常会遇到条目拖拽排序的需求,特别是在新闻类应用中就更普遍了.其实,我们在网上可以搜到许多关于拖拽排序的自定义控件, ...

  3. Android 仿QQ 聊天消息拖拽效果

    可拖拽的气泡效果 自定义view WateView public class WateView extends FrameLayout {//定义一个文本控件private TextView text ...

  4. android开发之仿QQ拖拽界面效果(侧滑面板)

    仿QQ拖拽界面效果(侧滑面板),我们一般继承Layout,不会直接去继承ViewGroup,而是继承FrameLayout,为什么五大布局我们偏偏只继承FrameLayout呢? 第一,FrameLa ...

  5. Android进阶之路 - 可拖拽的悬浮按钮

    类似文章在CSDN上有很多,但是几经查找之后原文其实产于简书的一位作者: 综合几篇文章,在原有基础上我会尽可能全面总结一下 效果图 实现思路 通过重写控件的onTouchEvent方法监听触摸效果 通 ...

  6. android 拖拽gridview,Android 可拖拽的GridView效果实现, 长按可拖拽和item实时交换

    具体的原理描述,可以去看夏神的这个博文 效果图: 这里,采用了gridview的onLongItemClickListener,来拿到长按的view,用于拖动 解决了,原来实现中可能遇到的item消失 ...

  7. Android 贝塞尔曲线实现QQ拖拽清除效果

    纯属好奇心驱动写的一个学习性Demo,效果如下: 这个小功能最重要的点在于起始点和触摸点之间的连接线绘制,它并不是一条单纯的直线,而是中间细两头粗的一条不规则的Path,而这个中间向内弯曲的效果正是一 ...

  8. Android 7.0 分屏拖拽文字和图片的研究

    一 前提 二 实测一 三 实测二 四 分析 1 分屏拖拽实现分析-Activity间 ViewstartDragAndDrop 实现拖拽图片的例子 2 拖拽实现分析-Activity内 一. 前提 1 ...

  9. android 高德拖拽地图定位,拖拽选址-拖拽选址-示例中心-JS API UI 组件示例 | 高德地图API...

    拖拽选址 html, body { height: 100%; margin: 0; width: 100%; padding: 0; overflow: hidden; font-size: 13p ...

最新文章

  1. js发送get、post请求的方法简介
  2. 卡巴斯基将支持微软企业安全解决方案Forefront
  3. 抖音一个老人和一个机器人歌曲_一个老人孤独去世,一个老人安然离世
  4. 打脸了!中通快递曾否认用假人充当安检员,官方反手一记实锤
  5. js学习总结--持续更新(2)
  6. 计算机网络学习笔记(22. Web应用概述)
  7. Linux系统基础开发应用及Linux-C用户手册
  8. proteus中的米字(14段)数码管用法
  9. 计算机小写换大写函数,excel小写换大写函数的教程
  10. 《活着》的优秀读后感范文3000字
  11. opencv之重映射remap
  12. android设置输入数字英文,android 安卓editext默认弹出英文输入法,只能输入英文与数字(示例代码)...
  13. 机器学习api_开发人员会喜欢的10种机器学习API
  14. 台积电2016年6月营收公布:股价飙升创台个股新记录
  15. Java 轮询(重发)机制
  16. oracle的dba_ segment,SEGMENT_TYPE TEMPORARY
  17. 【UE4】创建首个关卡
  18. 海信电视出现信息服务器,电视机一直显示启动中是什么原因_海信电视一直系统启动...
  19. [汇] 立即寻址,直接寻址,间接寻址
  20. GNU Radio3.8创建OOT的详细过程(python)

热门文章

  1. 弧度与角度的转换公式推导
  2. 指针进阶(指针与数组传参、数组指针与指针数组、函数指针数组、回调函数的辨析)
  3. 计算机网络管理 常见的计算机网络管理工具snmputil,Mib browser,SNMPc管理软件的功能和异同
  4. 使用javascript计算1加到100的结果
  5. C++绘制Koch曲线
  6. 懒逼 神经所 蒲慕明_中科院神经所所长蒲慕明:我内心从来都认为自己是中国人...
  7. 【Python】NICONICO Video Downloader / NICONICO Video视频下载工具
  8. TDA4VM/VH 芯片 NAVSS0
  9. 《Spring Cloud与Docker微服务架构实战》读书笔记
  10. 专升本管理学知识点总结——各种管理理论学说