美文网首页AndroidUI效果仿写android开发技巧
自定义地图下方可拖拽列表布局DragUpDownLinearLa

自定义地图下方可拖拽列表布局DragUpDownLinearLa

作者: AmStrong_ | 来源:发表于2018-07-17 09:38 被阅读87次

    这个控件是仿制高德地图下面的可拖拽列表栏做的。实现主要就是一个LinearLayout响应用户手势拖拽,有全屏,半屏,和隐藏三个模式。依据拖拽到松手的位置的y坐标占屏幕的百分比来确定对应的模式位置,再利用动画移动到对应的模式位置。
    完整的代码我会贴在文末。


    DragUpDownLinearlayout.gif

    一、确定三个模式的位置

    我这里使用的是铺满contentView,占contentView的1/3,和全部在隐藏在下面只留一个拖拽条三个模式。contentView的概念我这里大概讲一下,android的布局是在decorView这个根布局下的,分为titleView和ContentView。
    titleView放的是ActionBar等位置,如果设置noActionBar就没有titleView的位置了。
    而contentView就是我们平时Activity里面onCreate中setContent的那个ContentView,相当于我们的内容布局的父布局,在这个控件我们计算主要依靠它来完成。
    三个模式的height也就是Y坐标值是:

     switch (customMode) {
                        case TOP_MODE:
                            top = topHeight;
                            bottom = getHeight() + topHeight;
                            break;
                        case MIDDLE_MODE:
                            top = middleHeight;
                            bottom = getHeight() + middleHeight;
                            break;
                        case BOTTOM_MODE:
                            int topUp = contentViewHeight - indicatorHeightPx;
                            top = topUp;
                            bottom = getHeight() + topUp;
                            break;
                    }
    

    主要看一个top的赋值 这个top就是我们要设给onLayout的参数,控件的顶部的y坐标。

    topHeight contentView里面除了这个控件之外 顶部还有其他控件占位置,我们需要加上这个控件的高度,不然会覆盖掉它,例如上面有一个自定义的标题栏没有加入到Toolbar的位置而是放在contentView里面,那么这个情况就需要被考虑。这个值我是由外部初始化的时候计算传入的。

    middleHeight 计算方法:

    contentViewHeight = ((Activity) getContext()).getWindow().
                        findViewById(Window.ID_ANDROID_CONTENT).getMeasuredHeight();
    middleHeight = (contentViewHeight / 3) * 2;
    

    上面说的是1/3但这里写的是2/3是因为android屏幕的Y坐标是向下的,我们需要在1/3的位置就需要让控件向下移动2/3。

    **topUp ** 相当于留一个indicatorHeightPx(那个灰色的长条)的位置 其他全部在屏幕下方。

    OK,位置的计算就只有这些了。

    二、拖拽控件

    接下来就是主要的功能拖拽了。
    这里需要用到手势类GestureDetector,不熟悉的同学可以去搜索一下看一看,它里面封装了各种手势的触发条件和触发回调,使用起来比自己重写onTouch再分类要有效率的多。它的使用就是在onTouch方法里将参数传递给它:

    public boolean onTouch(View view, MotionEvent event) {
            mGestureDetector.onTouchEvent(event);
            return true;
        }
    

    它的实现类:

        @Override
        public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float distanceX, float distanceY) {
            int y = (int) motionEvent1.getY();
            // 获取本次移动的距离
            int dy = y - y0;
            int top = getTop();
            int bottom = getBottom();
            if (top <= topHeight && dy < 0) {
                // 高出顶部 则不改变位置防止超出顶部
                return false;
            }
            layout(getLeft(), (top + dy),
                    getRight(), (bottom + dy));
            isScrolling = true;
            return false;
        }
    
        @Override
        public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float x, float speedY) {
            float v = motionEvent1.getRawY() - rawYDown;
            switch (customMode) {
                case TOP_MODE:
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() + (middleHeight - getY()));
                    customMode = MIDDLE_MODE;
                    break;
                case MIDDLE_MODE:
                    if (v > 0) {
                        animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                                getTranslationY(), getTranslationY() + contentViewHeight - getY() - indicatorHeightPx);
                        customMode = BOTTOM_MODE;
                    } else {
                        animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                                getTranslationY(), getTranslationY() - getY() + topHeight);
                        customMode = TOP_MODE;
                    }
                    break;
                case BOTTOM_MODE:
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() + (middleHeight - getY()));
                    customMode = MIDDLE_MODE;
                    break;
                default:
            }
    
            animator.setDuration(500);
            animator.start();
            // 动画结束时,将控件的translation偏移量转化为Top值,便于计算
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    float translationY = getTranslationY();
                    setTranslationY(0);
                    layout(getLeft(), (int) (getTop() + translationY),
                            getRight(), (int) (getBottom() + translationY));
                    animator = null;
                }
            });
            isScrolling = false;
            hasFiling = true;
            return true;
        }
    

    就使用了这两个方法。思路就是在onScroll里面响应拖拽调用layout方法不断修改布局位置,然后结束的时候通常情况下回触发onFiling方法,在这个方法里计算位置开始动画等将控件移动到指定的位置。
    还需要注意的是当你慢慢拖拽的时候会触发不了onFiling这个方法 所以我在这里添加了一个hasFiling的标志位去判断onFiling是否调用了,没调用的话在onTouch里面再处理一下:

     @Override
        public boolean onTouch(View view, MotionEvent event) {
            mGestureDetector.onTouchEvent(event);
            // 是否有执行filing
            if (event.getAction() == MotionEvent.ACTION_UP) {
                if (!hasFiling) {
                    isScrolling = false;
                    // 松手时固定位置 计算占屏幕的百分比
                    float yUP = getTop();
                    float i = yUP / screenHeight;
                    if (i < 0.30) {
                        animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                                getTranslationY(), getTranslationY() - getY() + topHeight);
                        customMode = TOP_MODE;
                    } else if (i < 0.75) {
                        animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                                getTranslationY(), getTranslationY() + (middleHeight - getY()));
                        customMode = MIDDLE_MODE;
                    } else {
                        animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                                getTranslationY(), getTranslationY() + contentViewHeight - getY() - indicatorHeightPx);
                        customMode = BOTTOM_MODE;
                    }
                    animator.setDuration(500);
                    animator.start();
                    // 动画结束时,将控件的translation偏移量转化为Top值,便于计算
                    animator.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            super.onAnimationEnd(animation);
                            float translationY = getTranslationY();
                            setTranslationY(0);
                            layout(getLeft(), (int) (getTop() + translationY),
                                    getRight(), (int) (getBottom() + translationY));
                            animator = null;
                        }
                    });
                }
            }
            return true;
        }
    

    整体的逻辑还是看文末的代码吧 这里只是介绍一下功能的实现。

    三、解决事件分发冲突

    一般在这里内部都会有一个ListView控件来展示数据,它与我们的这个控件就会有滑动冲突。
    解决方法是用外部拦截法来解决。
    我在这里新建了一个接口来回调给调用类

        public void setInterceptCallBack(RequestInterceptCallBack interceptCallBack) {
            this.interceptCallBack = interceptCallBack;
        }
    
        public interface RequestInterceptCallBack {
            boolean canIntercept(boolean isDown);
        }
        
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            boolean intercept = false;
            switch (ev.getAction()) {
    
                case MotionEvent.ACTION_DOWN:
                    y0 = (int) ev.getY();
                    rawYDown = ev.getRawY();
                    intercept = false;
                    hasFiling = false;
                    break;
    
                case MotionEvent.ACTION_MOVE:
                    float dy = ev.getY() - y0;
                    Log.i(TAG, "dy" + dy);
                    if (Math.abs(dy) < 7 || animator != null || (customMode == TOP_MODE && dy < 0)) {
                        // 移动过小视为点击事件。不拦截 或者 动画尚未结束 本次不拦截
                        intercept = false;
                    } else if (dy > 0) {
                        intercept = interceptCallBack.canIntercept(true);
                    } else {
                        intercept = interceptCallBack.canIntercept(false);
                    }
                    break;
    
                case MotionEvent.ACTION_UP:
                    intercept = false;
                    break;
            }
            return intercept;
        }
    

    canIntercept(boolean isDown)这个参数我设置的是手势是否下滑,如果是recyclerView则在方法重写里面判断
    recyclerView.canScrollVertically(-1);这个方法。
    例如:

    @Override
        public boolean canIntercept(boolean isDown) {
            if (isDown) {
                Log.i(TAG, "-1: " + recyclerView.canScrollVertically(-1));
                return !recyclerView.canScrollVertically(-1);
            } else {
                Log.i(TAG, "1: " + recyclerView.canScrollVertically(1));
                return !recyclerView.canScrollVertically(1);
            }
        }
    

    其中的逻辑需要自己揣摩一下。

    四、最后调用类的初始化工作

    在Activity中:

      dragUpDownLinearLayout.setInterceptCallBack(this);
                dragUpDownLinearLayout.setTopHeight(relativeLayout.getMeasuredHeight());
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        dragUpDownLinearLayout.setLocation(DragUpDownLinearLayout.MIDDLE_MODE);
                        dragUpDownLinearLayout.setVisibility(View.VISIBLE);
                    }
                });  
    

    我这里是直接设置的Middle_Mode模式。布局里设置的空间隐藏,设置完再显示,不这样的话会出现闪一下的变化位置,比较不好,其实也可以进入的时候走一个动画。这些都看爱好和需求吧。

    代码

    /**
     * Created by Vito 
     */
    public class DragUpDownLinearLayout extends LinearLayout implements View.OnTouchListener,
            GestureDetector.OnGestureListener {
        public final static String TAG = "DragUpDownLinearLayout";
        public final static int TOP_MODE = 1;
        public final static int MIDDLE_MODE = 2;
        public final static int BOTTOM_MODE = 3;
        public int customMode = 0;
        // 手势监听对象
        private GestureDetector mGestureDetector;
        // 拖拽条的高度
        private final static int indicatorHeight = 30;
        private int indicatorHeightPx;
        // 中间位置的高度
        private int middleHeight;
        // contentView(去掉状态栏、toolbar和导航栏部分)的高度
        private int contentViewHeight;
        // 顶部其他控件的高度
        private int topHeight;
        // 屏幕的高度
        private float screenHeight;
        // 滑动开始手指落点
        private int y0;
        private float rawYDown;
        // 第一次加载标志位
        private boolean isFirstLayout = true;
        // 是否拦截事件接口回调,用于判断子控件的是否可滑动
        private RequestInterceptCallBack interceptCallBack;
        // 动画对象
        private ObjectAnimator animator = null;
        private static final String ANIMATOR_MODE = "translationY";
        // 是否触发了Filing方法,未触发交由onTouch方法完成移动
        private boolean hasFiling;
        // 是否在滚动触发的layout的标志位
        private boolean isScrolling;
    
        public DragUpDownLinearLayout(Context context) {
            this(context, null);
        }
    
        public DragUpDownLinearLayout(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public DragUpDownLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context);
        }
    
        @SuppressLint("ClickableViewAccessibility")
        private void init(Context context) {
            // 界面
            indicatorHeightPx = dp2px(indicatorHeight);
            setBackgroundColor(Color.WHITE);
            FrameLayout frameLayout = new FrameLayout(context);
            frameLayout.setLayoutParams(
                    new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, indicatorHeightPx));
            addView(frameLayout);
            View view = new View(context);
            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(dp2px(75), dp2px(8));
            params.gravity = Gravity.CENTER;
            view.setLayoutParams(params);
            view.setBackgroundResource(R.drawable.shape_drag_up_down_indicator);
            frameLayout.addView(view);
            // 获取屏幕的高
            DisplayMetrics dm = context.getResources().getDisplayMetrics();
            screenHeight = dm.heightPixels;
            setOnTouchListener(this);
            mGestureDetector = new GestureDetector(getContext(), this);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            boolean intercept = false;
            if (interceptCallBack != null) {
                switch (ev.getAction()) {
    
                    case MotionEvent.ACTION_DOWN:
                        y0 = (int) ev.getY();
                        rawYDown = ev.getRawY();
                        intercept = false;
                        hasFiling = false;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        float dy = ev.getY() - y0;
                        Log.i(TAG, "dy" + dy);
                        if (Math.abs(dy) < 7 || animator != null || (customMode == TOP_MODE && dy < 0)) {
                            // 移动过小视为点击事件。不拦截 或者 动画尚未结束 本次不拦截
                            intercept = false;
                        } else if (dy > 0) {
                            intercept = interceptCallBack.canIntercept(true);
                        } else {
                            intercept = interceptCallBack.canIntercept(false);
                        }
                        break;
    
                    case MotionEvent.ACTION_UP:
                        intercept = false;
                        break;
                }
            }
            return intercept;
        }
    
        @Override
        public boolean onTouch(View view, MotionEvent event) {
            mGestureDetector.onTouchEvent(event);
            // 是否有执行filing
            if (event.getAction() == MotionEvent.ACTION_UP) {
                if (!hasFiling) {
                    isScrolling = false;
                    // 松手时固定位置 计算占屏幕的百分比
                    float yUP = getTop();
                    float i = yUP / screenHeight;
                    if (i < 0.30) {
                        animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                                getTranslationY(), getTranslationY() - getY() + topHeight);
                        customMode = TOP_MODE;
                    } else if (i < 0.75) {
                        animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                                getTranslationY(), getTranslationY() + (middleHeight - getY()));
                        customMode = MIDDLE_MODE;
                    } else {
                        animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                                getTranslationY(), getTranslationY() + contentViewHeight - getY() - indicatorHeightPx);
                        customMode = BOTTOM_MODE;
                    }
                    animator.setDuration(500);
                    animator.start();
                    // 动画结束时,将控件的translation偏移量转化为Top值,便于计算
                    animator.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            super.onAnimationEnd(animation);
                            float translationY = getTranslationY();
                            setTranslationY(0);
                            layout(getLeft(), (int) (getTop() + translationY),
                                    getRight(), (int) (getBottom() + translationY));
                            animator = null;
                        }
                    });
                }
            }
            return true;
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            Log.e(TAG, "onLayout" + t);
            if (isFirstLayout) {
                contentViewHeight = ((Activity) getContext()).getWindow().
                        findViewById(Window.ID_ANDROID_CONTENT).getMeasuredHeight();
                middleHeight = (contentViewHeight / 3) * 2;
                isFirstLayout = false;
                Log.e(TAG, "contentViewHeight" + contentViewHeight);
            } else {
                Log.e(TAG, "isScrolling" + isScrolling);
                if (!isScrolling) {
                    switch (customMode) {
                        case TOP_MODE:
                            t = topHeight;
                            b = getHeight() + topHeight;
                            break;
                        case MIDDLE_MODE:
                            t = middleHeight;
                            b = getHeight() + middleHeight;
                            break;
                        case BOTTOM_MODE:
                            int topUp = contentViewHeight - indicatorHeightPx;
                            t = topUp;
                            b = getHeight() + topUp;
                            break;
                    }
                    setTop(t);
                    setBottom(b);
                }
            }
            super.onLayout(changed, l, t, r, b);
        }
    
        /**
         * 设置位置,同于指定初始化位置
         */
        public void setLocation(int mode) {
            switch (mode) {
                case TOP_MODE:
                    layout(getLeft(),
                            topHeight,
                            getRight(),
                            getHeight() + topHeight);
                    customMode = TOP_MODE;
                    break;
                case MIDDLE_MODE:
                    layout(getLeft(), middleHeight,
                            getRight(), middleHeight + getHeight());
                    customMode = MIDDLE_MODE;
                    break;
                case BOTTOM_MODE:
                    int topUp = contentViewHeight - indicatorHeightPx;
                    layout(getLeft(), topUp,
                            getRight(), topUp + getHeight());
                    customMode = BOTTOM_MODE;
                    break;
            }
    
        }
    
        @Override
        public boolean onDown(MotionEvent motionEvent) {
    
            return false;
        }
    
    
        @Override
        public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float distanceX, float distanceY) {
            int y = (int) motionEvent1.getY();
            // 获取本次移动的距离
            int dy = y - y0;
            int top = getTop();
            int bottom = getBottom();
            if (top <= topHeight && dy < 0) {
                // 高出顶部 则不改变位置防止超出顶部
                return false;
            }
            layout(getLeft(), (top + dy),
                    getRight(), (bottom + dy));
            isScrolling = true;
            return false;
        }
    
        @Override
        public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float x, float speedY) {
            float v = motionEvent1.getRawY() - rawYDown;
            switch (customMode) {
                case TOP_MODE:
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() + (middleHeight - getY()));
                    customMode = MIDDLE_MODE;
                    break;
                case MIDDLE_MODE:
                    if (v > 0) {
                        animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                                getTranslationY(), getTranslationY() + contentViewHeight - getY() - indicatorHeightPx);
                        customMode = BOTTOM_MODE;
                    } else {
                        animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                                getTranslationY(), getTranslationY() - getY() + topHeight);
                        customMode = TOP_MODE;
                    }
                    break;
                case BOTTOM_MODE:
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() + (middleHeight - getY()));
                    customMode = MIDDLE_MODE;
                    break;
                default:
            }
    
            animator.setDuration(500);
            animator.start();
            // 动画结束时,将控件的translation偏移量转化为Top值,便于计算
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    float translationY = getTranslationY();
                    setTranslationY(0);
                    layout(getLeft(), (int) (getTop() + translationY),
                            getRight(), (int) (getBottom() + translationY));
                    animator = null;
                }
            });
            isScrolling = false;
            hasFiling = true;
            return true;
        }
    
        @Override
        public void onLongPress(MotionEvent motionEvent) {
        }
    
        @Override
        public void onShowPress(MotionEvent motionEvent) {
        }
    
        @Override
        public boolean onSingleTapUp(MotionEvent motionEvent) {
            return false;
        }
    
        private int dp2px(float dipValue) {
            final float scale = getContext().getResources().getDisplayMetrics().density;
            return (int) (dipValue * scale + 0.5f);
        }
    
        public void setInterceptCallBack(RequestInterceptCallBack interceptCallBack) {
            this.interceptCallBack = interceptCallBack;
        }
    
        public interface RequestInterceptCallBack {
            boolean canIntercept(boolean isDown);
        }
    
        /**
         * 重新请求一次contentView 因为toolbar将它往下顶了一部分,也就是加一个偏移量
         */
        public void resetContentViewHeight(int off) {
            contentViewHeight = ((Activity) getContext()).getWindow().
                    findViewById(Window.ID_ANDROID_CONTENT).getMeasuredHeight() - off;
            middleHeight = (contentViewHeight / 3) * 2;
            Log.e(TAG, "resetContentViewHeight" + contentViewHeight);
        }
    
        /**
         * 设置顶部高度
         */
        public void setTopHeight(int topHeight) {
            this.topHeight = topHeight;
        }
    }
    
    

    相关文章

      网友评论

        本文标题:自定义地图下方可拖拽列表布局DragUpDownLinearLa

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