简单实现炫酷的滑动返回效果

博文出处:简单实现炫酷的滑动返回效果,欢迎大家关注我的博客,谢谢!
前言
======
在如今 app 泛滥的年代里,越来越多的开发者注重用户体验这个方面了。其中,有很多的 app 都有一种功能,那就是滑动返回。比如知乎、百度贴吧等,用户在使用这一类的 app 都可以滑动返回上一个页面。不得不说这个设计很赞,是不是心动了呢?那就继续往下看吧!

在GitHub上有实现该效果的开源库 SwipeBackLayout ,可以看到该库发展得已经非常成熟了。仔细看源码你会惊奇地发现其中的奥秘,没错,正是借助了 ViewDragHelper 来实现滑动返回的效果。ViewDragHelper 我想不必多说了,在我的博客中有很多的效果都是通过它来实现的。那么,下面我们就使用 ViewDragHelper 来实现这个效果吧。

自定义属性

首先,我们应该先定义几个自定义属性,比如说支持用户从左边或者右边滑动返回,丰富用户的选择性。所以现在 attrs.xml 中定义如下属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SwipeBackLayout">
        <attr name="swipe_mode" format="enum">
            <enum name="left" value="0"/>
            <enum name="right" value="1"/>
        </attr>
    </declare-styleable>
</resources>

从上面的 xml 中可知,定义了一个枚举属性,左边为0,右边为1。

然后主角 SwipeBackLayout 就要登场了。

public class SwipeBackLayout extends FrameLayout {

    private ViewDragHelper mViewDragHelper;
    // 主界面
    private View mainView;
    // 主界面的宽度
    private int mainViewWidth;
    // 模式,默认是左滑
    private int mode = MODE_LEFT;
    // 监听器
    private SwipeBackListener listener;
    // 是否支持边缘滑动返回, 默认是支持
    private boolean isEdge = true;

    private int mEdge;
    // 阴影Drawable
    private Drawable shadowDrawable;
    // 阴影Drawable固有宽度
    private int shadowDrawbleWidth;
    // 已经滑动的百分比
    private float movePercent;
    // 滑动的总长度
    private int totalWidth;
    // 默认的遮罩透明度
    private static final int DEFAULT_SCRIM_COLOR = 0x99000000;
    // 遮罩颜色
    private int scrimColor = DEFAULT_SCRIM_COLOR;
    // 透明度
    private static final int ALPHA = 255;

    private Paint mPaint;
    /**
     * 滑动的模式,左滑
     */
    public static final int MODE_LEFT = 0;
    /**
     * 滑动的模式,右滑
     */
    public static final int MODE_RIGHT = 1;
    // 最小滑动速度
    private static final int MINIMUM_FLING_VELOCITY = 400;

    private static final String TAG = "SwipeBackLayout";

    public SwipeBackLayout(Context context) {
        this(context, null);
    }

    public SwipeBackLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SwipeBackLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipeBackLayout);
        // 得到滑动模式,默认左滑
        mode = a.getInt(R.styleable.SwipeBackLayout_swipe_mode, MODE_LEFT);
        a.recycle();
        initView();
    }

    ...

}

initView

在构造器主要做的就是得到滑动模式,默认是左边滑动。之后调用 initView() 。那么我们来看看 initView() 的代码:

// 初始化阴影Drawable
private void initShadowView() {
    if (Build.VERSION.SDK_INT >= 21) {
        shadowDrawable = getResources().getDrawable(mode == MODE_LEFT ? R.drawable.shadow_left : R.drawable.shadow_right, getContext().getTheme());
    } else {
        shadowDrawable = getResources().getDrawable(mode == MODE_LEFT ? R.drawable.shadow_left : R.drawable.shadow_right);
    }
    if (shadowDrawable != null) {
        shadowDrawbleWidth = shadowDrawable.getIntrinsicWidth();
    }
}

private void initView() {
    float density = getResources().getDisplayMetrics().density;
    // 最小滑动速度
    float minVel = density * MINIMUM_FLING_VELOCITY;
    initShadowView();
    mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {

        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return mainView == child; // 只有是主界面时才可以被滑动
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            // 根据模式区分
            switch (mode) {
                case MODE_LEFT:  // 左边
                    if (left < 0) {
                        return 0;
                    } else if (Math.abs(left) > totalWidth) {
                        return totalWidth;
                    } else {
                        return left;
                    }
                case MODE_RIGHT:  // 右边
                    if (left > 0) {
                        return 0;
                    } else if (Math.abs(left) > totalWidth) {
                        return -totalWidth;
                    } else {
                        return left;
                    }
                default:
                    return left;
            }
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            switch (mode) {
                case MODE_LEFT:
                    movePercent = left * 1f / totalWidth;  // 滑动的进度
                    Log.i(TAG, "movePercent = " + movePercent);
                    break;
                case MODE_RIGHT:
                    movePercent = Math.abs(left) * 1f / totalWidth;
                    Log.i(TAG, "movePercent = " + movePercent);
                    break;
            }
        }

        @Override
        public int getViewHorizontalDragRange(View child) {
            if (mode == MODE_LEFT) {
                return Math.abs(totalWidth);
            } else {
                return -totalWidth;
            }
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            switch (mode) {
                case MODE_LEFT:
                    if (xvel > -mViewDragHelper.getMinVelocity() && Math.abs(releasedChild.getLeft()) > mainViewWidth / 2.0f) {  // 如果当前已经滑动超过子View宽度的一半,并且速度符合预期设置
                        swipeBackToFinish(totalWidth, 0);  // 把当前界面finish
                    } else if (xvel > mViewDragHelper.getMinVelocity()) {
                        swipeBackToFinish(totalWidth, 0);
                    } else {
                        swipeBackToRestore();  // 当前界面回到原位
                    }
                    break;
                case MODE_RIGHT:
                    if (xvel < mViewDragHelper.getMinVelocity() && Math.abs(releasedChild.getLeft()) > mainViewWidth / 2.0f) {
                        swipeBackToFinish(-totalWidth, 0);
                    } else if (xvel < -mViewDragHelper.getMinVelocity()) {
                        swipeBackToFinish(-totalWidth, 0);
                    } else {
                        swipeBackToRestore();
                    }
                    break;
            }
        }
    });
    // 设置最小滑动速度
    mViewDragHelper.setMinVelocity(minVel);
}

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    int count = getChildCount();
    if (count == 1) { // 子View只能有一个
        // 获取子view
        mainView = getChildAt(0);
    } else {
        throw new IllegalArgumentException("the child of swipebacklayout can not be empty and must be the one");
    }
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    // 得到主界面的宽度
    mainViewWidth = w;
    //总长度,包含了mainView的宽度以及阴影图片的宽度
    totalWidth = mainViewWidth + shadowDrawbleWidth;
}

initView() 中,设置了 mViewDragHelper 的最小滑动速度,并且设置了 mViewDragHelper 回调的接口。回调接口中的方法都有注释,相信大家应该都能看懂。另外在 initView() 中初始化了阴影图片,以备下面中使用。

drawChild

想要阴影在滑动中绘制出来,我们必须重写 drawChild(Canvas canvas, View child, long drawingTime) 方法,并且在 onTouchEvent(MotionEvent event)invalidate() ,保证用户滑动过程中调用 drawChild(Canvas canvas, View child, long drawingTime) 方法。

@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    Log.i(TAG, "" + (mViewDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE));
    if (child == mainView && mViewDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) {
        // 绘制阴影
        drawShadowDrawable(canvas, child);
        // 绘制遮罩层
        drawScrimColor(canvas, child);
    }
    return super.drawChild(canvas, child, drawingTime);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    mViewDragHelper.processTouchEvent(event);
    // 重绘,保证在滑动的时候可以绘制阴影
    invalidate();
    return true;
}

drawChild(Canvas canvas, View child, long drawingTime) 中调用 drawShadowDrawable(Canvas canvas, View child) 来绘制阴影以及 drawScrimColor(Canvas canvas, View child) 来绘制遮罩层。下面分别是两个方法的源码:

// 绘制阴影
private void drawShadowDrawable(Canvas canvas, View child) {
    Rect drawableRect = new Rect();
    // 得到mainView的矩形
    child.getHitRect(drawableRect);
    // 设置shadowDrawable绘制的矩形
    if (mode == MODE_LEFT) { // 左滑
        shadowDrawable.setBounds(drawableRect.left - shadowDrawbleWidth, drawableRect.top, drawableRect.left, drawableRect.bottom);
    } else { // 右滑
        shadowDrawable.setBounds(drawableRect.right, drawableRect.top, drawableRect.right + shadowDrawbleWidth, drawableRect.bottom);
    }
    // 设置shadowDrawable的透明度,最低为0.3
    shadowDrawable.setAlpha((int) ((1 - movePercent > 0.3 ? 1 - movePercent : 0.3) * ALPHA));
    // 将shadowDrawable绘制在canvas上
    shadowDrawable.draw(canvas);
}

// 绘制遮罩层
private void drawScrimColor(Canvas canvas, View child) {
    // 根据滑动进度动态设置透明度
    int baseAlpha = (scrimColor & 0xFF000000) >>> 24;
    int alpha = (int) (baseAlpha * (1 - movePercent));
    int color = alpha << 24 | (scrimColor & 0xffffff);
    // 设置绘制矩形区域
    Rect rect;
    if (mode == MODE_LEFT) { // 左滑
        rect = new Rect(0, 0, child.getLeft(), getHeight());
    } else { // 右滑
        rect = new Rect(child.getRight(), 0, getWidth(), getHeight());
    }
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(color);
    canvas.drawRect(rect, mPaint);
}

mainView 、阴影、遮罩层的关系示意图如下:

关系示意图

onViewReleased

看完了上面的两个方法的代码,最后就是当用户手指抬起时判断逻辑的代码了:

/**
 * 滑动返回,结束该View
 */
public void swipeBackToFinish(int finalLeft, int finalTop) {
    if (mViewDragHelper.smoothSlideViewTo(mainView, finalLeft, finalTop)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
    if (listener != null) {
        listener.onSwipeBackFinish();
    }
}

/**
 * 滑动回归到原位
 */
public void swipeBackToRestore() {
    if (mViewDragHelper.smoothSlideViewTo(mainView, 0, 0)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}

@Override
public void computeScroll() {
    super.computeScroll();
    if (mViewDragHelper.continueSettling(true)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}

public interface SwipeBackListener {
    /**
     * 该方法会在滑动返回完成的时候回调
     */
    void onSwipeBackFinish();
}

/**
 * 设置滑动返回监听器
 *
 * @param listener
 */
public void setSwipeBackListener(SwipeBackListener listener) {
    this.listener = listener;
}

相应的代码还是比较简单的,主要使用了 smoothSlideViewTo(View view, int left, int top) 的方法来滑动到指定位置。若是结束当前界面的话,回调监听器的接口。

啰嗦了这么多,我们来看看运行时的效果图吧:

滑动返回效果gif

尾语

好了,SwipeBackLayout 大致的逻辑就是上面这样子的。整体来说还是比较通俗易懂的,而且对 ViewDragHelper 熟悉的人会发现,使用 ViewDragHelper 自定义一些 ViewGroup 的套路都是大同小异的。以后想要自定义一些 ViewGroup 都是得心应手了。

如果对此有疑问的话可以在下面留言。

最后,国际惯例,附上 SwipeBackLayout Demo 的源码:

SwipeBackDemo.rar

Goodbye!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容