自定义View之地图下方可拖拽列表布局DragUpDownLinearLayout

本文介绍如何实现一个仿高德地图底部可拖拽列表的自定义View,详细讲解了确定布局模式、拖拽逻辑、事件分发冲突解决及初始化步骤。通过GestureDetector处理手势,动态调整布局位置并实现动画效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

一、确定三个模式的位置

我这里使用的是铺满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;
   
DragLinearLayout是一个LinearLayout, 他可是其子View在其范围内可拖动、可交换位置。默认情况下,子View是不可拖动的,你需要调用DragLinearLayout.setViewDraggable(child, child)方法让其可拖动。项目地址:https://github.com/justasm/DragLinearLayout 效果图:如何使用和使用LinearLayout一样:                        2. 让子View可拖动默认情况下子View是不可拖动的,你需要调用dragLinearLayout.setViewDraggable()让子View变为可拖动的。DragLinearLayout dragLinearLayout = (DragLinearLayout) findViewById(R.id.container); //让子view可拖动,默认是不能拖动的         for(int i = 1; i < dragLinearLayout.getChildCount(); i ){             View child = dragLinearLayout.getChildAt(i);             dragLinearLayout.setViewDraggable(child, child);          }可以动态添加可拖动子viewfinal View view = View.inflate(context, R.layout.view_layout, null); dragLinearLayout.addDragView(viewview.findViewById(R.id.view_drag_handle));   // ..   dragLinearLayout.removeDragView(view);使用OnViewSwapListener检测子view之间的排序变化事件:dragLinearLayout.setOnViewSwapListener(new DragLinearLayout.OnViewSwapListener() {     @Override     public void onSwap(View firstView, int firstPosition,             View secondView, int secondPosition) {         // update data, etc..     } });当在ScrollView中使用DragLinearLayout的时候,如果你想在拖拽的时候ScrollView也能滚动,需要调用setContainerScrollView(Scroll
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值