簡單實現炫酷的滑動返回效果

博文出處:簡單實現炫酷的滑動返回效果,歡迎大家關注我的博客,謝謝!
前言
======
在如今 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!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容