Android 右滑隱藏佈局、上下滑切換顯示資料

奏響曲發表於2018-01-03

自定義佈局ScrollMenu

目錄
簡介
功能
主要程式碼介紹
如何使用
ScrollMenu全部程式碼
專案地址
總結
圖紙

簡介

  • 這個自定義的view,繼承RelativeLayout(原因現在大部分父佈局用的都是RelativeLayout)
  • 通過Scroller實現滑動
  • 通過速度跟蹤器獲取滑動速度
  • 通過設定子控制元件tag排除特殊情況

功能

  1. 實現右滑隱藏
  2. 上下滑動切換顯示資料的監聽(在監聽中更換資料)
  3. 排除了RecyclerView垂直和水平滑動和ScrollMenu的衝突
  4. 通過為子佈局設定特定的tag解決衝突(因為還有ScrollView等沒有加入判斷,需要自行設定tag排除衝突)
  5. 可以設定是否能水平滑動或是否能垂直方向滑動

主要程式碼介紹


    private boolean
            canVerticalSlide, //能否垂直方向滑動
            canHorizontalSlide,//能否水平方向滑動
            openVerticalSlide = true,//開啟垂直方向滑動
            openHorizontalSlide = true;//開啟水平方向的滑動
複製程式碼
  • 在ScrollMenu中通過條件判斷此時是否正水平和垂直滑動canHorizontalSlide、canVerticalSlide,通過控制這兩個來控制能否滑動
  • 通過openVerticalSlide、openHorizontalSlide在activity中呼叫這兩個變數的set方法,來間接控制canHorizontalSlide、canVerticalSlide的值

double angle = Math.atan2(Math.abs(ev.getY() - angleLastY), Math.abs(ev.getX() - angleLastX)) * 180 / Math.PI;
複製程式碼
  • 計算滑動的角度
                    canHorizontalSlide = canHorizontalSlide && angle < 30;
                    canVerticalSlide = canVerticalSlide && angle > 30;
複製程式碼
  • 如果角度小於30°則水平能滑動,垂直方向不能滑動
  • 如果角度大於30°則垂直能滑動,水平方向不能滑動

    /**
     * 計算(x, y)座標是否在child view的範圍內
     *
     * @param child 子佈局
     * @param x     x座標
     * @param y     y座標
     * @return 子佈局是否在點選範圍內
     */
    public boolean isTouchPointInView(View child, int x, int y) {
        int[] location = new int[2];
        child.getLocationOnScreen(location);
        int top = location[1];
        int left = location[0];
        int right = left + child.getMeasuredWidth();
        int bottom = top + child.getMeasuredHeight();
        return y >= top && y <= bottom && x >= left && x <= right;
    }
複製程式碼
  • 計算點選(x,y)座標是否在此子佈局範圍之內

View view = getTargetView(this, (int) ev.getRawX(), (int) ev.getRawY());
複製程式碼
  • 獲取點選位置的佈局(只獲取RecyclerView、或設定了tag:no_vertical、no_horizontal的佈局)

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            $e(String.format("computeScroll mScroller --- currX:%d --- currY:%d", mScroller.getCurrX(), mScroller.getCurrY()));
            if (-getHeight() == mScroller.getCurrY()) {
                mScrollHandler.sendEmptyMessage(ScrollHandler.FAST_BOTTOM_TO_NORMAL);
            }

            if (getHeight() == mScroller.getCurrY()) {
                mScrollHandler.sendEmptyMessage(ScrollHandler.FAST_TOP_TO_NORMAL);
            }
            invalidate();
        }
    }
複製程式碼
  • 當Scroller呼叫startScroll方法後,會不斷的呼叫computeScroll通過不斷的呼叫scrollTo高頻率的重新整理顯示試圖
  • (-getHeight() == mScroller.getCurrY())true表示滑出底部
  • getHeight() == mScroller.getCurrY()true表示為滑出頂部

    public void toRight() {
        status = RIGHT;
        $e("toRight getScrollX = " + getScrollX());
        mScroller.startScroll(getScrollX(), 0, -(getWidth() + getScrollX()), 0, 1000);
        invalidate();
    }

    public void toTop() {
        status = TOP;
        mScroller.startScroll(0, getScrollY(), 0, -getScrollY() + getHeight(), 1000);
        invalidate();
    }

    public void toBottom() {
        status = BOTTOM;
        mScroller.startScroll(0, getScrollY(), 0, -(getHeight() + getScrollY()), 1000);
        invalidate();
    }

    public void toNormal() {
        if (status == TOP || status == BOTTOM) {
            mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), 1000);
        } else {
            $e("toLeft getScrollX = " + getScrollX());
            mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 1000);
        }
        invalidate();
        status = NORMAL;
    }
複製程式碼
  • toRight() :向右滑動
  • toTop():向頂部外滑動
  • toBottom():向底部外滑動
  • toNormal(): 向正常顯示狀態滑動

如何使用

  1. 監聽上下滑動完成後的事件監聽(用來更新顯示的資料)
        scrollMenu.setOnScrollCompleteListener(new ScrollMenu.OnScrollCompleteListener() {
            @Override
            public void completeTop() {
                Toast.makeText(MainActivity.this, "↑↑上滑切換↑↑", Toast.LENGTH_SHORT).show();
                changeData(true);
            }

            @Override
            public void completeBottom() {
                Toast.makeText(MainActivity.this, "↓↓下滑切換↓↓", Toast.LENGTH_SHORT).show();
                changeData(false);
            }
        });
複製程式碼
  1. 開啟或關閉橫向縱向滑動,示例程式碼如下
        ctvH.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ctvH.toggle();
                ctvH.setText(ctvH.isChecked() ? "橫向滑動開" : "橫向滑動關");
                scrollMenu.setOpenHorizontalSlide(ctvH.isChecked());
            }
        });

        ctvV.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ctvV.toggle();
                ctvV.setText(ctvV.isChecked() ? "縱向滑動開" : "縱向滑動關");
                scrollMenu.setOpenVerticalSlide(ctvV.isChecked());
            }
        });
複製程式碼
  1. 需要解決滑動時的衝突(RecyclerView解決了水平和垂直情況可不用考慮),為子控制元件設定tag,例如下面的ScrollView(垂直方向不滑動no_vertical, 水平方向不滑動no_horizontal
        <ScrollView
            android:id="@+id/scrollView"
            android:layout_width="100dp"
            android:layout_height="match_parent"
            android:layout_below="@id/rvHorizontal"
            android:tag="no_vertical"
            android:background="@android:color/holo_red_light">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="sdjaflkjsdlakfjlknsdfjsadljfldsjafnsdfjfdsadfsadfsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafoijoijsoadifnoisdajofihosadhfoihsoidfnoisadhgiouasho;eiwnfoiewahfioaewboeifwbgwoeibfoieawbngfiownfdsafsj" />
        </ScrollView>
複製程式碼

ScrollMenu全部程式碼

package com.example.jiana.scrollmenudemo;

import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.Scroller;

import java.lang.ref.WeakReference;
import java.util.Timer;
import java.util.TimerTask;


public class ScrollMenu extends RelativeLayout {
    private boolean isOpenLog = true;//是否開啟log
    /**
     * 設定tag為"no_horizontal"的子佈局觸控無法水平滑動
     */
    private static final String VIEW_TAG_NO_VERTICAL = "no_vertical";
    /**
     * 設定tag為"no_vertical"的子佈局觸控無法垂直滑動
     */
    private static final String VIEW_TAG_NO_HORIZONTAL = "no_horizontal";

    /**
     * 正常狀態
     */
    public static final int NORMAL = 0;
    /**
     * 側滑到頂部
     */
    public static final int TOP = 2;
    /**
     * 滑到右側
     */
    public static final int RIGHT = 3;
    /**
     * 側滑到底部
     */
    public static final int BOTTOM = 4;
    private static final String TAG = "ScrollMenu";
    //滑動元件
    private Scroller mScroller;
    //數度跟蹤者
    private VelocityTracker mVelocityTracker;

    //最後一個動作的位置
    private float mLastTouchX, mLastTouchY;
    //能被拖動的臨界值
    private int mTouchSlop;
    //滑動的最大速度
    private int mMaximumVelocity;
    private float angleLastX, angleLastY;
    //拖動鎖
    private boolean mDragging = false;
    private boolean
            canVerticalSlide, //能否垂直方向滑動
            canHorizontalSlide,//能否水平方向滑動
            openVerticalSlide = true,//開啟垂直方向滑動
            openHorizontalSlide = true;//開啟水平方向的滑動

    /**
     * 當前狀態
     */
    private int status = NORMAL;

    private ScrollHandler mScrollHandler;

    public ScrollMenu(Context context) {
        super(context);
        init(context);
    }

    public ScrollMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ScrollMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        mScrollHandler = new ScrollHandler(this);
        mScroller = new Scroller(context);
        mVelocityTracker = VelocityTracker.obtain();
        //獲取系統觸控的臨界常量值
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            $e(String.format("computeScroll mScroller --- currX:%d --- currY:%d", mScroller.getCurrX(), mScroller.getCurrY()));
            if (-getHeight() == mScroller.getCurrY()) {
                mScrollHandler.sendEmptyMessage(ScrollHandler.FAST_BOTTOM_TO_NORMAL);
            }

            if (getHeight() == mScroller.getCurrY()) {
                mScrollHandler.sendEmptyMessage(ScrollHandler.FAST_TOP_TO_NORMAL);
            }
            invalidate();
        }
    }


    /**
     * 初始化滾動和開始繪製
     */
    public void toRight() {
        status = RIGHT;
        $e("toRight getScrollX = " + getScrollX());
        mScroller.startScroll(getScrollX(), 0, -(getWidth() + getScrollX()), 0, 1000);
        invalidate();
    }

    public void toTop() {
        status = TOP;
        mScroller.startScroll(0, getScrollY(), 0, -getScrollY() + getHeight(), 1000);
        invalidate();
    }

    public void toBottom() {
        status = BOTTOM;
        mScroller.startScroll(0, getScrollY(), 0, -(getHeight() + getScrollY()), 1000);
        invalidate();
    }

    public void toNormal() {
        if (status == TOP || status == BOTTOM) {
            mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), 1000);
        } else {
            $e("toLeft getScrollX = " + getScrollX());
            mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 1000);
        }
        invalidate();
        status = NORMAL;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            canHorizontalSlide = openHorizontalSlide;
            canVerticalSlide = openVerticalSlide;
            View view = getTargetView(this, (int) ev.getRawX(), (int) ev.getRawY());
            $e("dispatchTouchEvent view = " + view);
            if (view != null) {
                if (view instanceof RecyclerView) {
                    RecyclerView rv = (RecyclerView) view;
                    canHorizontalSlide = openHorizontalSlide && !rv.getLayoutManager().canScrollHorizontally();
                    canVerticalSlide = openVerticalSlide && !canHorizontalSlide;
                } else if (VIEW_TAG_NO_VERTICAL.equals(view.getTag())) {
                    canHorizontalSlide = openHorizontalSlide;
                    canVerticalSlide = false;
                } else if (VIEW_TAG_NO_HORIZONTAL.equals(view.getTag())) {
                    canHorizontalSlide = false;
                    canVerticalSlide = openVerticalSlide;
                }

                $e("dispatchTouchEvent canHorizontalSlide = " + canHorizontalSlide);
                $e("dispatchTouchEvent " +
                        "canVerticalSlide = " + canVerticalSlide);
            }

            if (onTouchDownListener != null) {
                onTouchDownListener.touch(ev);
            }
        }
        return super.dispatchTouchEvent(ev);
    }

    /**
     * 監聽向子佈局傳遞的觸控事件和攔截事件
     * 如果子佈局是互動式的(如button),將仍然能接收到觸控事件
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        $e(String.format("onInterceptTouchEvent action = %d, x = %f, y = %f", ev.getAction(), ev.getX(), ev.getY()));
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //判斷是否已經完成滾動,如果滾動則停止
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                //重置速度跟蹤器
                mVelocityTracker.clear();
                mVelocityTracker.addMovement(ev);

                //儲存初始化觸控位置
                mLastTouchX = ev.getX();
                mLastTouchY = ev.getY();
                angleLastX = ev.getX();
                angleLastY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                final float x = ev.getX();
                final float y = ev.getY();
                final int xDiff = (int) Math.abs(x - mLastTouchX);
                final int yDiff = (int) Math.abs(y - mLastTouchY);
                $e("onInterceptTouchEvent xDiff = " + xDiff);
                $e("onInterceptTouchEvent yDiff = " + yDiff);
                //計算角度
                double angle = Math.atan2(Math.abs(ev.getY() - angleLastY), Math.abs(ev.getX() - angleLastX)) * 180 / Math.PI;
                //驗證移動距離是否足夠成為觸發拖動事件
                if (xDiff > mTouchSlop || yDiff > mTouchSlop) {
                    canHorizontalSlide = canHorizontalSlide && angle < 30;
                    canVerticalSlide = canVerticalSlide && angle > 30;

                    if (!canVerticalSlide && !canHorizontalSlide) {
                        return super.onInterceptTouchEvent(ev);
                    }

                    mDragging = true;
                    mVelocityTracker.addMovement(ev);
                    $e("onInterceptTouchEvent 獲取這個動作事件");
                    //獲取這個事件
                    return true;
                }

                break;
            case MotionEvent.ACTION_CANCEL:
                break;
            case MotionEvent.ACTION_UP:
                mDragging = false;
                mVelocityTracker.clear();
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    /**
     * 處理接收的事件(事件由onInterceptTouchEvent獲取)
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        $e(String.format("onTouchEvent action = %d, x = %f, y = %f", event.getAction(), event.getX(), event.getY()));
        mVelocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //獲取後續事件
                return true;
            case MotionEvent.ACTION_MOVE:
                move(event);
                break;
            case MotionEvent.ACTION_CANCEL:
                mDragging = false;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_UP:
                mDragging = false;
                //計算當前的速度,如果速度大於最小數度臨界值則開啟一個滑動
                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int velocityX = (int) mVelocityTracker.getXVelocity();
                int velocityY = (int) mVelocityTracker.getYVelocity();
                $e("onTouchEvent MotionEvent.ACTION_UP velocityX = " + velocityX);
                $e("onTouchEvent MotionEvent.ACTION_UP velocityY = " + velocityY);
                $e("onTouchEvent getScrollX() = " + getScrollX());
                $e("onTouchEvent getScrollY() = " + getScrollY());
                if (canHorizontalSlide) {
                    if (velocityX >= 5000 || (velocityX >= 0 && getScrollX() <= -getWidth() / 3) || (velocityX < 0 && velocityX > -5000 && getScrollX() < -getWidth() * 2 / 3)) {
                        toRight();
                    } else {
                        toNormal();
                    }
                } else if (canVerticalSlide) {
                    if (velocityY >= 5000 || (velocityY >= 0 && getScrollY() <= -getHeight() / 4)) {
                        toBottom();
                        break;
                    }

                    if ((velocityY < -5000 && status == NORMAL) || (velocityY < 0 && getScrollY() >= getHeight() / 4)) {
                        toTop();
                        break;
                    }

                    toNormal();
                }

                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 處理移動事件
     */
    private void move(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        //水平滾動距離
        float diffX = mLastTouchX - x;
        //垂直方向滑動的距離
        float diffY = mLastTouchY - y;

        //如果可以拖動是否被鎖,x與y移動的距離大於可移動的距離
        $e("onTouchEvent mDragging = " + mDragging);
        if (!mDragging && (Math.abs(diffX) > mTouchSlop || Math.abs(diffY) > mTouchSlop)) {
            mDragging = true;
        }

        //計算角度
        double angle = Math.toDegrees(Math.atan2(Math.abs(y - angleLastY), Math.abs(x - angleLastX)));
        $e("onTouchEvent angle = " + angle);

        if (mDragging) {
            //滑動這個view
            if (canHorizontalSlide && angle < 30) {
                scrollBy((int) diffX, 0);
                mLastTouchX = x;
                canVerticalSlide = false;
            } else if (canVerticalSlide && angle > 30) {
                scrollBy(0, (int) diffY);
                mLastTouchY = y;
                canHorizontalSlide = false;
            }
        }
    }

    /**
     * 根據觸控到文字獲得具體的子view
     */
    public View getTargetView(View view, int x, int y) {
        View target = null;
        ViewGroup viewGroup = (ViewGroup) view;
        for (int i = 0, len = viewGroup.getChildCount(); i < len; i++) {
            View child = viewGroup.getChildAt(i);
            if (child instanceof RecyclerView) {
                target = isTouchPointInView(child, x, y) ? child : null;
                if (target != null) {
                    break;
                }
            } else if (child instanceof ViewGroup) {
                View v = getTargetView(child, x, y);
                if (v != null) {
                    return v;
                }
            }

            target = (isTouchPointInView(child, x, y) && (VIEW_TAG_NO_VERTICAL.equals(child.getTag()) || VIEW_TAG_NO_HORIZONTAL.equals(child.getTag()))) ? child : null;
            if (target != null) {
                break;
            }
        }
        return target;
    }


    /**
     * 計算(x, y)座標是否在child view的範圍內
     *
     * @param child 子佈局
     * @param x     x座標
     * @param y     y座標
     * @return 子佈局是否在點選範圍內
     */
    public boolean isTouchPointInView(View child, int x, int y) {
        int[] location = new int[2];
        child.getLocationOnScreen(location);
        int top = location[1];
        int left = location[0];
        int right = left + child.getMeasuredWidth();
        int bottom = top + child.getMeasuredHeight();
        return y >= top && y <= bottom && x >= left && x <= right;
    }

    public int getStatus() {
        return status;
    }


    private OnScrollCompleteListener onScrollCompleteListener;
    private OnTouchDownListener onTouchDownListener;

    public void setOnTouchDownListener(OnTouchDownListener l) {
        this.onTouchDownListener = l;
    }

    public void setOnScrollCompleteListener(OnScrollCompleteListener l) {
        this.onScrollCompleteListener = l;
    }

    public interface OnScrollCompleteListener {
        void completeTop();

        void completeBottom();
    }

    public interface OnTouchDownListener {
        void touch(MotionEvent ev);
    }

    public void setOpenVerticalSlide(boolean openVerticalSlide) {
        this.openVerticalSlide = openVerticalSlide;
    }

    public void setOpenHorizontalSlide(boolean openHorizontalSlide) {
        this.openHorizontalSlide = openHorizontalSlide;
    }

    private static class ScrollHandler extends Handler {
        /**
         * 快速恢復正常模式
         */
        public static final int FAST_TOP_TO_NORMAL = 0X12345;
        public static final int FAST_BOTTOM_TO_NORMAL = 0X12346;

        private WeakReference<ScrollMenu> wr;
        private boolean isRun;

        public ScrollHandler(ScrollMenu scrollMenu) {
            wr = new WeakReference<>(scrollMenu);
        }

        @Override
        public void handleMessage(Message msg) {
            ScrollMenu mScrollMenu = wr.get();
            if (mScrollMenu == null) {
                return;
            }

            switch (msg.what) {
                case FAST_BOTTOM_TO_NORMAL:
                    mScrollMenu.scrollTo(0, -mScrollMenu.getHeight());
                    mScrollMenu.invalidate();
                    mScrollMenu.scrollTo(0, 0);
                    if (mScrollMenu.onScrollCompleteListener != null && agreeOperated()) {
                        mScrollMenu.onScrollCompleteListener.completeBottom();
                    }
                    break;
                case FAST_TOP_TO_NORMAL:
                    mScrollMenu.scrollTo(0, mScrollMenu.getHeight());
                    mScrollMenu.invalidate();
                    mScrollMenu.scrollTo(0, 0);
                    if (mScrollMenu.onScrollCompleteListener != null && agreeOperated()) {
                        agreeOperated();
                        mScrollMenu.onScrollCompleteListener.completeTop();
                    }
                    break;
            }
        }

        /**
         * 是否同意操作
         */
        private boolean agreeOperated() {
            if (isRun) {
                return false;
            }
            isRun = true;
            Timer tExit = new Timer();
            tExit.schedule(new TimerTask() {
                @Override
                public void run() {
                    isRun = false;
                }
            }, 1000);
            return true;
        }
    }

    /**
     * 列印log
     *
     * @param s 列印的log資料
     */
    private void $e(String s) {
        if (isOpenLog) {
            Log.e(TAG, s);
        }
    }
}
複製程式碼

專案地址

github:https://github.com/xujiaji/ScrollMenuDemo

總結

  • 總體上來看功能實現,正常流暢滑動
  • 細節上需要考慮很多其他使用情況,如果沒有匯入RecyclerView會報錯,因為預設判斷排除了RecyclerView橫向和縱向的滑動

圖紙

ScrollMenu圖紙

相關文章