你见过微信侧滑返回的联动效果,但开门效果、百叶窗效果见过吗?

3,627 阅读7分钟

SmartSwipe是一个Android侧滑处理框架,它封装了对控件侧滑事件(上/下/左/右4个方向滑动的手势事件)的捕获、分发及多点交替滑动的处理,基于SmartSwipe我们可以为控件添加各种你想要的侧滑效果。

先来看看它能做些什么吧!

如果已经了解SmartSwipe的功能,只是想了解他的实现原理
可跳过第一节,直接看第二节的原理介绍

一、 用法及演示

1.1 一行代码实现全局侧滑返回

//仿手机QQ的手势滑动返回
SmartSwipeBack.activityStayBack(application, null);		
//仿微信带联动效果的透明侧滑返回
SmartSwipeBack.activitySlidingBack(application, null);	
//侧滑开门样式关闭activity
SmartSwipeBack.activityDoorBack(application, null);		
//侧滑百叶窗样式关闭activity
SmartSwipeBack.activityShuttersBack(application, null);	
//仿小米MIUI系统的贝塞尔曲线返回效果
SmartSwipeBack.activityBezierBack(application, null);

侧滑返回的更多用法请戳 这里

效果图:

侧滑返回效果

1.2 一行代码让页面动起来

//为控件添加仿iOS的弹性留白效果:
//当纵向不能滚动(或滚动到顶/底)时,若继续拖动,则UI呈现弹性留白效果,释放后平滑恢复
SmartSwipe.wrap(view)
    .addConsumer(new SpaceConsumer())
    .enableVertical();

效果图:

弹性留白效果

1.3 一行代码让页面具有弹性

//为控件添加仿MIUI的弹性拉伸效果:
//当纵向不能滚动(或滚动到顶/底)时,若继续拖动,则UI呈现弹性拉伸效果,释放后平滑恢复
SmartSwipe.wrap(view)
    .addConsumer(new StretchConsumer())
    .enableVertical();

效果图:

弹性拉伸效果

1.4 一行代添加下拉刷新

//xxxMode第二个参数为false,表示工作方向为纵向:下拉刷新&上拉加载更多
//如果第二个参数设置为true,则表示工作方向为横向:右拉刷新&左拉加载更多
SmartSwipeRefresh.drawerMode(view, false).setDataLoader(loader);
SmartSwipeRefresh.behindMode(view, false).setDataLoader(loader);
SmartSwipeRefresh.scaleMode(view, false).setDataLoader(loader);
SmartSwipeRefresh.translateMode(view, false).setDataLoader(loader);

下拉刷新的更多用法请戳 这里

样式 效果图
drawerMode
drawerMode
behindMode
behindMode
scaleMode
scaleMode
translateMode
translateMode

1.5 一行代码添加滑动菜单

SmartSwipe.wrap(view)
    //添加抽屉效果,其效果与DrawerLayout相似
    //  DrawerLayout只支持左右2个方向,而DrawerConsumer支持上下左右4个方向
    .addConsumer(new DrawerConsumer())	
    //设置横向(左右两侧)的抽屉为同一个view(常见的侧滑显示删除按钮的功能)
    .setHorizontalDrawerView(buttonsViewGroup) 
    .setScrimColor(0x2F000000) //设置遮罩的颜色
    .setShadowColor(0x80000000)	//设置边缘的阴影颜色
    ;

效果图:

滑动菜单

1.6 一行代码添加具有联动效果的滑动菜单

SmartSwipe.wrap(view)
    .addConsumer(new SlidingConsumer())
    .setRelativeMoveFactor(0.3F) //联动系数
    .setHorizontalDrawerView(buttonsView)
    .setScrimColor(0x2F000000)
    ;

效果图:

可联动滑动菜单

1.7 炫酷的封面

SmartSwipe.wrap(coverView)
    .addConsumer(new ShuttersConsumer()) //百叶窗效果
    .setScrimColor(0xAF000000)
    .enableAllDirections()
    .addListener(new SimpleSwipeListener() {
        @Override
        public void onSwipeOpened(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction) {
            //封面打开后自动隐藏或移除
            wrapper.setVisibility(View.GONE);
        }
    });

效果图:

百叶窗封面

SmartSwipe.wrap(coverView)
    .addConsumer(new DoorConsumer()) //开门效果
    .setScrimColor(0xAF000000)
    .enableAllDirections()
    .addListener(new SimpleSwipeListener() {
        @Override
        public void onSwipeOpened(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction) {
            //封面打开后自动隐藏或移除
            wrapper.setVisibility(View.GONE);
        }
    });

效果图:

开门封面

关于封面的更多设置请参考: Demo

二、实现原理

2.1 先介绍一下ViewDragHelper

ViewDragHelper是Android官方支持库中有一个工具类。它可以帮助我们处理控件的拖拽:先创建一个自定义ViewGroup,将被拖动的控件添加到这个自定义ViewGroup中,并用ViewDragHelper来处理控件的拖拽。

ViewDragHelper的主要作用是:拦截父容器的touch事件,捕获一个子控件来进行拖拽,通过改变这个子控件的left和top来将其在父容器中重新定位,从而达到拖拽的效果。

在官方支持库中,滑动抽屉相关的SlidingPaneLayout和DrawerLayout,以及CoordinatorLayout布局相关的BottomSheetBehavior和SwipeDismissBehavior,都能看到ViewDragHelper的身影。

但是,ViewDragHelper的名称也表明它就是用来处理拖拽的,拖拽的对象必须是一个子View,在拖拽的过程中需要改变子控件的left和top,对于一些没有子View被拖拽的侧滑效果(例如:MIUI系统的贝塞尔曲线侧滑返回效果、手机QQ的侧滑返回效果及MIUI官方app中的普遍使用了的弹性拉伸效果等等),却有点力有不逮。

2.2 借鉴ViewDragHelper实现侧滑处理

针对侧滑这个手势,我们能不能将它的概念抽象一下,到底侧滑指的是什么呢?

  • 狭义侧滑:从屏幕的某个边缘开始向着远离该边缘的方向滑动
  • 广义侧滑:手指在屏幕上按下之后向着某个方向滑动

我的理解是,广义侧滑包含狭义侧滑,只不过是触发区域是否在屏幕边缘的区别罢了。

既然侧滑手势能被明确地抽象出来,那么我们是否可以借鉴ViewDragHelper的事件拦截思路将它做这样的封装?

对被侧滑控件的touch事件进行拦截分析,确认是否将其捕获作为侧滑手势
然后计算好侧滑的实时位移(手指滑动的位移,而不是不依赖于View的left与top)
再通过策略模式(Strategy Pattern)使用不同的策略不断消费侧滑的位移来进行侧滑效果的UI呈现。

答案是肯定的!

2.3 SmartSwipe的实现原理

SmartSwipe在ViewDragHelper的基础上,将它对子View的捕获及移动处理改造成对父View自身触摸事件的定性(能否及是否捕获)、定向(捕获的事件所触发的侧滑方向)及定位(事件捕获之后在侧滑方向上移动的距离),并将侧滑距离交由SwipeConsumer来消费,SwipeConsumer根据侧滑距离的变化对控件布局进行相应的改变。

SmartSwipe的封装思路如下:

  • 用一个ViewGroup将需要处理侧滑事件的控件View包裹起来(被包裹起来的控件作为它的contentView
  • 可以为这个ViewGroup添加一些附属控件(如:滑动抽屉
  • 拦截这个ViewGroup的touch事件,并将touch事件转换为侧滑距离交给SwipeConsumer进行消费
  • SwipeConsumer根据侧滑距离的变化对控件布局进行相应的改变
  • 通过继承SwipeConsumer,用不同的方式来改变控件布局(例如:对contentView及附属控件的位置、缩放、透明等进行改变),从而实现各种侧滑的效果。

于是,侧滑的手势事件识别及滑动距离计算的工作在框架内部就统一完成了,至于根据侧滑距离来实现各种不同的UI呈现效果,就可以很方便地通过继承SwipeConsumer来实现了。

2.4 如何创建自定义SwipeConsumer?

以框架内置的仿MIUI系统应用中弹性拉伸效果的实现为例

根据侧滑距离,对contentView进行缩放和平移,从而实现弹性拉伸效果

代码如下:

public class StretchConsumer extends SwipeConsumer {
    @Override
    public void onDetachFromWrapper() {
        super.onDetachFromWrapper();
        View contentView = mWrapper.getContentView();
        if (contentView != null) {
            contentView.setScaleX(1);
            contentView.setScaleY(1);
            contentView.setTranslationX(0);
            contentView.setTranslationY(0);
        }
    }

    @Override
    public void onDisplayDistanceChanged(int distanceXToDisplay, int distanceYToDisplay, int dx, int dy) {
        View contentView = mWrapper.getContentView();
        if (contentView != null) {
            if (distanceXToDisplay >= 0 && isLeftEnable() || distanceXToDisplay <= 0 && isRightEnable()) {
                contentView.setScaleX(1 + Math.abs((float) distanceXToDisplay) / mWidth);
                contentView.setTranslationX(distanceXToDisplay / 2F);
            }
            if (distanceYToDisplay >= 0 && isTopEnable() || distanceYToDisplay <= 0 && isBottomEnable()) {
                contentView.setScaleY(1 + Math.abs((float) distanceYToDisplay) / mHeight);
                contentView.setTranslationY(distanceYToDisplay / 2F);
            }
        }
    }
}

以上就是实现弹性拉伸效果的全部代码,很简单,不是吗?

它的使用方式同样简单:

SmartSwipe.wrap(view) //指定目标控件
    .addConsumer(new StretchConsumer()) //添加弹性拉伸效果
    .enableVertical(); //指定工作方向为:上、下2个方向

再来看看仿手机QQ侧滑返回的效果如何实现

手机QQ侧滑时UI没有任何变化
在手指释放时,根据滑动的方向和速率来决定是否finish当前Activity

代码如下:

public class StayConsumer extends SwipeConsumer {
    private int mMinVelocity = 1000;

    public StayConsumer() {
        //不能通过滑动距离判断是否需要打开
        setOpenDistance(Integer.MAX_VALUE)
                .setMaxSettleDuration(0); //打开时无需动画,时间置为0
    }

    @Override
    protected void onDisplayDistanceChanged(int distanceXToDisplay, int distanceYToDisplay, int dx, int dy) {
        //滑动时不需要对contentView做任何改变
    }

    @Override
    public void onSwipeReleased(float xVelocity, float yVelocity) {
        //在释放时,根据速率和方向来决定是否打开
        if (Math.abs(xVelocity) > Math.abs(yVelocity)) {
            if (mDirection == DIRECTION_LEFT && xVelocity >= mMinVelocity || (mDirection == DIRECTION_RIGHT && xVelocity <= -mMinVelocity)) {
                //置为打开状态
                mCurSwipeDistanceX = getSwipeOpenDistance();
                mProgress = 1;
            }
        } else {
            if (mDirection == DIRECTION_TOP && yVelocity >= mMinVelocity || (mDirection == DIRECTION_BOTTOM && yVelocity <= -mMinVelocity)) {
                //置为打开状态
                mCurSwipeDistanceY = getSwipeOpenDistance();
                mProgress = 1;
            }
        }
        super.onSwipeReleased(xVelocity, yVelocity);
    }

    public int getMinVelocity() {
        return mMinVelocity;
    }

    //支持使用者设置最低速率的阈值
    public StayConsumer setMinVelocity(int minVelocity) {
        if (minVelocity > 0) {
            this.mMinVelocity = minVelocity;
        }
        return this;
    }
}

是不是也很简单!

点击这里了解创建自定义SwipeConsumer的详细步骤

小结

本文介绍了SmartSwipe侧滑处理框架的使用方式及实现原理,并通过2个示例介绍了自定义侧滑效果的方法。

只是文中的示例是较为简单的侧滑效果,至于复杂的侧滑效果实现介绍,如果读者们需要的话,我接下来另外写一篇文章来单独介绍,如有需要,请给我留言!

另外,Star一个开源项目是对它最好的鼓励和支持!