美文网首页RILAndroid开发程序员
Android-右滑隐藏布局、上下滑切换显示数据

Android-右滑隐藏布局、上下滑切换显示数据

作者: 灵魂奏响曲 | 来源:发表于2016-06-17 01:21 被阅读1711次
    自定义布局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图纸

    相关文章

      网友评论

      • 258f297360fe:目前上下切换是空白的,想要在上下切换的时候 ,加个背景,有什么思路吗?
        灵魂奏响曲: @Android小哥 不是这意思!是把图片放在ScrollMenu布局的下面,这样滑动ScroollMenu就跟图片没有关系了。
        258f297360fe:@雨雪纷飞 这样好像不行,因为用的是Scroller,所以无论怎么滑,都只是ScrollMenu在里面滑。
        灵魂奏响曲: @Android小哥 用帧布局,下面放图片,上面放这个布局,你觉得怎么样

      本文标题:Android-右滑隐藏布局、上下滑切换显示数据

      本文链接:https://www.haomeiwen.com/subject/asnmdttx.html