写点什么

轻松实现微信滑动返回页面效果

作者:Changing Lin
  • 2022 年 6 月 20 日
  • 本文字数:5964 字

    阅读完需:约 20 分钟

轻松实现微信滑动返回页面效果

1.项目概要:

本文主要讲解如何 轻松实现微信的页面的滑动返回 的交互效果,会讲解涉及到的知识点和工作原理,介绍Android系统提供的2种API的使用方法和应用场景,以及两者之间的差异。
复制代码

2.背景和需求

有次,产品经理跟我讨论,微信的页面返回动画是怎么实现的?能否在我们的产品上面应用这种交互效果,并且要求尽量减少集成和使用成本。于是,开始了微信的页面返回动画效果的研究。
复制代码


  • 主流的 APP 的页面结构,顶部有一个 ActionBar 或者 Toolbar 的标题栏,下边是我们的页面自定义内容。标题栏里面由 2 部分组成,一个是返回按钮,另一个是标题。通过监听返回按钮的点击事件,即可返回上一个页面,实现页面切换和用户交互。典型 APP 标题栏结构如下图所示。


  • 主流 APP 的页面返回方式对比:

  • APP 类型:常规应用、Google 官方应用、IDE 新建 Demo

  • 返回方式:1.点击返回按钮 2.系统导航栏返回按钮

  • 实现原理:1.监听 Button 的 OnClickListener,在调用 Activity.finish()或 Activity.onBackPress()

  • 特点:点击即可自动返回,这个动作只会产生一个效果


  • APP 类型:微信

  • 返回方式:1.点击标题栏返回按钮 2.系统导航栏返回按钮 3.从页面左边沿开始向右活动返回

  • 实现原理:1.前 2 种方法同上 2.第 3 种方法,是本文研究的重点

  • 特点:除了同上特点,还支持动画、拖拽,可能停留在当前页面,或者返回上一页面,增加了互动效果和用户粘性


  • APP 类型:美团

  • 返回方式:1.点击标题栏返回按钮 2.系统导航栏返回按钮

  • 实现原理:同第一种应用

  • 特点:点击即可自动返回,这个动作只会产生一个效果


  • 需求:如何轻松实现微信滑动返回页面效果

3.实现原理

3.1 定义:

  • 用户的手指触摸屏幕,从左边沿往右滑动;当手指在屏幕上移动时,当前页面会跟随手指移动;当手指离开屏幕时,会根据松手时的滑动速度、和松手的位置 2 个条件来决定是停留在当前页面还是返回上一页面。细心的读者可实际观察下交互效果。

3.2 需求分析:

  • 对用户的手指触摸进行跟踪:View.onTouchEvent,View.onInterceptTouchEvent

  • 对页面进行移动:View.scrollTo/View.scrollBy,OverScroller,Animation

  • 停留在当前页面:把页面移动的距离反向移动即可

  • 返回上一层页面:把当前页面回收即可

  • 总结:根据上面的分析,这个功能本质上是关于触摸反馈、滑动、动画等知识点的综合应用,一种方法是利用 View.onTouchEvent+View.scrollTo/View.scrollBy 来实现;另一种方法,Android 系统其实给我们提供了对 View 进行拖拽的 API,如:ViewDragHelperOnDragListener

3.3 ViewDragHelper 的使用方法:

  • 定义:ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number of useful operations and state tracking for allowing a user to drag and reposition views within their parent ViewGroup. 是一个用于开发通用 ViewGroup 工具类,提供了许多用户在 ViewGroup 内部拖拽和移动子 View 的方法和状态跟踪的方法。

  • 需要创建⼀个 ViewDragHelper 和 ViewDragHelper.Callback()

  • 需要写在 ViewGroup 里面,重写 onInterceptTouchEvent() 和 onTouchEvent()

  • 例子:


// 1.新建内部类BeSmartDragCallback,继承自 ViewDragHelper.Callback  private inner class BeSmartDragCallback : ViewDragHelper.Callback() {    override fun tryCaptureView(child: View, pointerId: Int): Boolean {    // 由ViewGroup决定 是否捕获当前用户点击的View,返回true表示捕获当前child      return true    }    override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {       // 由ViewGroup来提供子View在拖拽过程中的水平位置,可用于控制在移动过程中子View如何跟随手指位置      return left    }    override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {       // 由ViewGroup来提供子View在拖拽过程中的垂直位置,可用于控制在移动过程中子View如何跟随手指位置      return top    }    override fun onViewCaptured(capturedChild: View, activePointerId: Int) {     // 当用户开始拖拽的时候的回调方法,可以在这里记录子View的位置信息,如:left,top    }    override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {      // 当用户拖拽到目标位置后,松手一刻的回调接口,xvel表示表示水平方向的滑动速度,yvel表示垂直方向的滑动速度    }  }
// 2.新建 ViewDragHelper private var dragHelper = ViewDragHelper.create(this,BeSmartDragCallback())
// 3.重写 ViewGroup.onInterceptTouchEvent()override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { return dragHelper.shouldInterceptTouchEvent(ev) }// 4.重写 ViewGroup.onTouchEvent()override fun onTouchEvent(event: MotionEvent): Boolean { dragHelper.processTouchEvent(event) return true }
复制代码

3.4 OnDragListener 的使用方法:

  • 定义:Interface definition for a callback to be invoked when a drag is being dispatched to this view. The callback will be invoked before the hosting view's own onDrag(event) method. 当 View 收到一个拖拽事件时,这个监听器的接口就会被调用;且先于被拖拽的子 View 的父控件的 onDrag(event)调用。

  • 通过 startDragAndDrop() 来启动拖拽

  • 用 View.setOnDragListener() 来监听:OnDragListener 内部只有一个方法:onDrag(event),ViewGroup.onDragEvent() 方法也会收到拖拽回调

  • 例子:


// 1.新建BeSmartDragListener内部类继承自OnDragListenerprivate inner class BeSmartDragListener: OnDragListener {    override fun onDrag(v: View, event: DragEvent): Boolean {      when (event.action) {        DragEvent.ACTION_DRAG_STARTED ->  {        }        DragEvent.ACTION_DRAG_ENTERED -> {        }        DragEvent.ACTION_DRAG_EXITED -> {        }        DragEvent.ACTION_DRAG_ENDED -> {        }      }      return true    }  }// 2.为子View设置监听器private var dragListener: OnDragListener =BeSmartDragListener()child.setOnDragListener(dragListener)
// 3.启动拖拽child.startDragAndDrop(null, DragShadowBuilder(v), v, 0)
复制代码

3.5 对比总结:

  • ViewDragHelper(support v4):

  • 应用场景:用户拖动 ViewGroup 中的某个子 View

  • 特点:需要拦截复空间的触摸事件,拖拽的对象是子 View 本身,关注的是子 View 的位置移动

  • 原理:实时修改被拖拽的子 View 的 位置参数(如:left, top 值)


  • OnDragListener(API 11):

  • 应用场景:用户的 拖起 -> 放下 操作,侧重点在于内容的移动,可以附加拖拽数据

  • 特点:是 View 自带的一个监听器,不需要父控件参与。关注的是事件,无法移动 View 的位置,拖动的只是一个 View 拷贝,支持跨进程传递数据

  • 原理:创造出⼀个图像在屏幕的最上层,图像跟随用户手指移动


4.使用方法

  • 有了前文的铺垫,我们开始实现微信滑动返回页面效果,因为需要对页面进行滑动,本质上是对 View 的移动,所以选择 ViewDragHelper 来实现。

4.1 新建 BeSmartViewDragHelperCallback:

private class BeSmartViewDragHelperCallback extends ViewDragHelper.Callback {        @Override        public void onEdgeDragStarted(int edgeFlags, int pointerId) {            super.onEdgeDragStarted(edgeFlags, pointerId);            Log.e(TAG, String.format("onEdgeDragStarted: %d-%d", edgeFlags, pointerId));            View originRootView = findViewById(R.id.origin_root_id);            if (null != originRootView)                viewDragHelper.captureChildView(originRootView, 0);        }        @Override        public void onEdgeTouched(int edgeFlags, int pointerId) {            super.onEdgeTouched(edgeFlags, pointerId);            Log.e(TAG, String.format("onEdgeTouched: %d-%d", edgeFlags, pointerId));        }        @Override        public void onViewDragStateChanged(int state) {            super.onViewDragStateChanged(state);            Log.e(TAG, "onViewDragStateChanged: " + state);        }        @Override        public boolean tryCaptureView(@NonNull View child, int pointerId) {            Log.e(TAG, String.format("tryCaptureView: %s-%d", child, child.getId()));            return false;        }        @Override        public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {            super.onViewCaptured(capturedChild, activePointerId);            Log.e(TAG, String.format("onViewCaptured: %s-%d", capturedChild, activePointerId));            isFinished = false;        }        @Override        public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {            super.onViewReleased(releasedChild, xvel, yvel);            Log.e(TAG, String.format("onViewReleased: %s-%f-%f", releasedChild, xvel, yvel));            int left = releasedChild.getLeft();            if (xvel > viewConfiguration.getScaledMinimumFlingVelocity()) {                viewDragHelper.settleCapturedViewAt(getWidth(), 0);                isFinished = true;            } else {                if (left > getWidth() / 2) {                    viewDragHelper.settleCapturedViewAt(getWidth(), 0);                    isFinished = true;                } else {                    viewDragHelper.settleCapturedViewAt(0, 0);                    isFinished = false;                }            }            postInvalidateOnAnimation();        }        @Override        public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {            return Math.max(left, 0);        }        @Override        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {            return 0;        }    }
复制代码

4.2 新建 ViewDragHelper:

private final BeSmartViewDragHelperCallback callback = new BeSmartViewDragHelperCallback();ViewDragHelper viewDragHelper = ViewDragHelper.create(watchViewGroup, callback);viewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); // 开启ViewDragHelper的左边沿跟踪功能ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
复制代码

4.3 重写父控件的 onInterceptTouchEvent 和 onTouchEvent:

@Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        return viewDragHelper.shouldInterceptTouchEvent(ev);    }
@Override public boolean onTouchEvent(MotionEvent event) { viewDragHelper.processTouchEvent(event); return true; }
@Override public void computeScroll() { super.computeScroll(); boolean result = viewDragHelper.continueSettling(true); if (result) { postInvalidateOnAnimation(); } else { if (null != consumer) consumer.accept(isFinished); } }
复制代码

4.4 封装 SmartReturnHelper 工具类:

public class SmartReturnHelper {    public static final String TAG = "DragHelperContent";
public static void attach(@NonNull Activity activity, Consumer<Boolean> consumer) { ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
// 思路是:1.获取到decorView,获取到第一个子View就是原先的LinearLayout // 2.先把originRoot从decorView中移除掉 // 3.中间嵌套一个我们的自定义WeChatLayout // 4.把originRoot添加到WeChatLayout,再把WeChatLayout添加到DecorView // 5.WeChatLayout就是实现我们触摸事件监听、页面滑动的ViewGroup View originRoot = decorView.getChildAt(0); decorView.removeViewAt(0);
WeChatLayout weChatLayout = new WeChatLayout(activity); originRoot.setId(R.id.origin_root_id); weChatLayout.setId(R.id.we_chat_layout_id); weChatLayout.addView(originRoot); weChatLayout.setConsumer(consumer); decorView.addView(weChatLayout, 0); }}
复制代码

5.使用方法

public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_wechat);
SmartReturnHelper.attach(this, isFinish -> { if (isFinish) finish(); }); }
public void onClick(View view) { startActivity(new Intent(getBaseContext(), EmptyActivity.class)); }}
复制代码

6.成果展示

该APP有2个页面,分别对每个页面进行嵌入SmartReturnHelper和WeChatLayout,来实现对用户手指触摸事件的监听,以及对页面的滑动效果,从而实现微信的滑动返回页面动画效果


  • 如上面的动图所示,用户手指从左边沿往右滑动,当前页面会跟随手指的移动而移动;

  • 当手指离开屏幕时,会自动检测此刻的状态,如果水平滑动速度>系统最小滑动速度,那么认为这是一个返回触发事件,使用属性动画把页面向右移动出屏幕,并回调 Consumer 的 accept 接口,finish 当前的 Activity。

  • 如果水平滑动速度<系统最小滑动速度,且滑动的最后一个位置的水平坐标大于屏幕宽度的 1/2,那么认为这是一个返回触发事件,执行上述流程。

  • 如果水平滑动速度<系统最小滑动速度,且滑动的最后一个位置的水平坐标小于屏幕宽度的 1/2,则认为用户还想停留在当前页面,使用属性动画把页面向左移动回原点。

7.参考文献

  • 待补充

  • 注意事项 1:需要配置页面的主题是透明背景:

 <style name="Theme.PlaygroundNoActionBar" parent="Theme.Playground">        <item name="android:windowIsTranslucent">true</item>        <item name="android:windowBackground">@android:color/transparent</item>        <item name="windowActionBar">false</item>        <item name="windowNoTitle">true</item>    </style>
复制代码
  • 注意事项 2:android:windowBackground 的值设置需为引用,不能直接设置为透明颜色值(#00000000),否则会失效。

发布于: 刚刚阅读数: 4
用户头像

Changing Lin

关注

获得机遇的手段远超于固有常规之上~ 2020.04.29 加入

我能做的,就是调整好自己的精神状态,以最佳的面貌去面对那些未曾经历过得事情,对生活充满热情和希望。

评论

发布
暂无评论
轻松实现微信滑动返回页面效果_android_Changing Lin_InfoQ写作社区