Android ViewDragHelper实现地图上滑布局

一、可行性分析

需求可行性:一般运用于地图页面的上滑,比如地图类 app,打车类 app,外卖类 app。

技术可行性:我们知道 Android View 的滑动有 2 个大类,一个是 ViewGroup 滑动(ScrollXXX)子 View 静止,最终修改的是RenderNode的图像偏移TranslationXXX; 另一类是子 View 滑动 但ViewGroup 不动,最终修改的是left\right\top\bottom实际布局位置 ,实际上两种方式都可以实现如下效果,相比来说第一类比较容易实现。本次使用的是第二类,借助 ViewDragHelper 实现,因为其本身提供了很多工具。

当然 ViewDragHelper 存在很多问题:

  • 1、不适合多个子 View 联动,因为 API 存在很多限制,使用不当容易产生丢帧问题,从而导致 View 错乱,需要多次矫正 View 位置
  • 2、不适合深入定制多个View联动的滑动效果,虽然本文克服了此问题
  • 3、无法捕获可滑动的 ViewGroup

简单来说,ViewDragHelper 更适合单个不可滑动 View 的操作,复杂联动显得有些鸡肋,本次开发中深有体会,如果非要实现复杂的 offset 联动效果(多个View一起滑动),建议舍弃 ViewDragHelper,直接使用事件处理或者 NestedScrolling 机制可能会会更好,直接事件处理可参考 ListView/RecyclerView,后者可参考 NestedScrollView。

从这两张图我们可以看出,左侧的Tab是吸顶的,而右侧整个Head部份是会完全展示的,两种功能,只需要简单的设置,就能改出符合需求的UI。

二、问题

难点:

  • 父子 View 事件转移机制:事件转移需要先 cancel/up 本次事件,然后重新 dispatch 一个 down 事件
  • Listview/recyclerView 事件捕获,ViewDragHelper 不会捕获该类 View,因此需要手动取捕获
  • 偏移计算,需要实现联动需要进行偏移计算,ViewDragHelper 只能处理单个 View,因此需要进行所有 view 的偏移计算

2.1 丢帧问题处理

主要原因是多个View级联动时,可能RecyclerView和ListView也在滑动,但是ViewDragHelper调用abort时,没发调用到RecyclerView和ListView内部的Scroller来终止滚动,因此需要手动处理,主要是滑动过程只能中需要实时补偿.

java 复制代码
    /**
     * DragerHelper无法暴漏Scroller,settle滑动状态下abort时会出现位置偏差,
     * 因此需要修复View联动时处理丢帧问题
     */
    private void fixLossFrame() {

        int childCount = getChildCount();
        int firstChildTop = getFirstChildTop();
        int firstChildHeight = getFirstChildHeight();
        View firstChildView = getChildAt(0);
        LayoutParams lp = (LayoutParams) firstChildView.getLayoutParams();
        int offsetTop = firstChildTop + firstChildHeight + lp.topMargin + lp.bottomMargin;

        for (int i = 1; i < childCount; i++) {
            View child = getChildAt(i);
            lp = (LayoutParams) child.getLayoutParams();
            int childTop = child.getTop();
            int expectTop = offsetTop + lp.topMargin;
            if (childTop != expectTop) {
                ViewCompat.offsetTopAndBottom(child, expectTop - childTop);
            }
            offsetTop += child.getHeight() + lp.topMargin + lp.bottomMargin;
        }


    }

2.2 无法捕获可滑动的 ViewGroup

主要原因是ListView和RecyclerView的事件捕获方式存在差异,导致优先级比较高,因此优先捕获事件。问题主要出现在下滑过程中无法捕获ListView、RecyclerView、ScrollView,上滑过程中事件被ListView、RecyclerView、ScrollView抢占。

java 复制代码
              float currentY = ev.getY();

                float dy = currentY - startEventY;
                View view = mDragHelper.findTopChildUnder((int) startEventX, (int) startEventY);
                if (view == null) {
                    break;
                }
                boolean isAtTop = isAtTop(view);
                if (!isAtTop) {
                    break;
                }
                //兼容向下滑动时,事件被传递给ListView,RecyclerView的问题
                if (dy > 0) {
                    Log.d("TouchEvent", "1----isAtTop==" + isAtTop);
                    mDragHelper.captureChildView(view, 0);
                    shouldAbortDragHelper = false;
                    return true;
                } else {

                    //兼容向上滑动时,事件被传递给ListView,RecyclerView的问题
                    int firstChildTop = getFirstChildTop();
                    int firstChildHeight = getFirstChildHeight();
                    int firstChildOffsetTop = firstChildTop;
                    if (allowHeaderOverScoll) {
                        firstChildOffsetTop += firstChildHeight;
                    }

                    if (firstChildOffsetTop > 0) {
                        mDragHelper.captureChildView(view, 0);
                        shouldAbortDragHelper = false;
                        return true;
                    }
                }

2.3 全部代码

下面是NestedScrollLayout完整实现,这里我们要注意的以下几点

  • 布局测量
  • 布局滑动
  • scrolling consume
java 复制代码
public class NestedScrollLayout extends FrameLayout implements View.OnTouchListener {


    private final ViewDragHelperCallback mViewDragCallback;
    private ViewDragHelper mDragHelper;
    private DisplayMetrics displayMetrics = null;
    private boolean shouldAbortDragHelper = false;

    float startEventY = 0;
    float startEventX = 0;

    float childStartEventX = 0f;
    float childStartEventY = 0f;

    //允许头部划出顶部
    private boolean allowHeaderOverScoll = false;

    //默认偏移基线
    private int defaultOffsetTop = 0;

    //是否首次布局
    private boolean isFirstLayout = true;

    private static final int MIN_FLING_VELOCITY = 500; // dips per second

    public NestedScrollLayout(Context context) {
        this(context, null);
    }

    public NestedScrollLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NestedScrollLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mViewDragCallback = new ViewDragHelperCallback(this);
        displayMetrics = getResources().getDisplayMetrics();
        mDragHelper = ViewDragHelper.create(this, mViewDragCallback);
        mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * displayMetrics.density);
        defaultOffsetTop = (int) dp2px(300);
        setWillNotDraw(false);

    }

    public float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }


    public void setAllowHeaderOverScoll(boolean allowHeaderOverScoll) {
        this.allowHeaderOverScoll = allowHeaderOverScoll;
    }

    public void setDefaultOffsetTop(int defaultOffsetTop) {

        if (defaultOffsetTop < 0) {
            defaultOffsetTop = 0;
        }
        this.defaultOffsetTop = defaultOffsetTop;
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        int childCount = getChildCount();
        if (childCount == 0) {
            return super.onInterceptTouchEvent(ev);
        }

        boolean shouldIntercept = mDragHelper.shouldInterceptTouchEvent(ev);
        if (shouldIntercept) {
            shouldAbortDragHelper = false;
            return true;
        }

        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                startEventY = ev.getY();
                startEventX = ev.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                float currentY = ev.getY();

                float dy = currentY - startEventY;
                View view = mDragHelper.findTopChildUnder((int) startEventX, (int) startEventY);
                if (view == null) {
                    break;
                }
                boolean isAtTop = isAtTop(view);
                if (!isAtTop) {
                    break;
                }
                //兼容向下滑动时,事件被传递给ListView,RecyclerView的问题
                if (dy > 0) {
                    Log.d("TouchEvent", "1----isAtTop==" + isAtTop);
                    mDragHelper.captureChildView(view, 0);
                    shouldAbortDragHelper = false;
                    return true;
                } else {

                    //兼容向上滑动时,事件被传递给ListView,RecyclerView的问题
                    int firstChildTop = getFirstChildTop();
                    int firstChildHeight = getFirstChildHeight();
                    int firstChildOffsetTop = firstChildTop;
                    if (allowHeaderOverScoll) {
                        firstChildOffsetTop += firstChildHeight;
                    }

                    if (firstChildOffsetTop > 0) {
                        mDragHelper.captureChildView(view, 0);
                        shouldAbortDragHelper = false;
                        return true;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                shouldAbortDragHelper = true;
                break;
        }

        return false;

    }
    /**
     * 是否可捕获当前view,如果所有子view位置偏移都在顶部,可捕获,
     * 如果类似ListView被滑动了,那么不要进行捕获
     * @return
     */
    private boolean shouldCaptureView() {
        int childCount = getChildCount();
        if (childCount == 0) return false;

        for (int i = 0; i < childCount; i++) {

            View childView = getChildAt(i);
            if (!isAtTop(childView)) {
                Log.d("shouldCaptureView","false");
                return false;
            }
        }
        return true;
    }


    private boolean isAtTop(View view) {
        if (view == null) return false;

        if (!(view instanceof ViewGroup)) {
            return true;
        }

        if (view instanceof ListView) {
            int firstVisiblePosition = ((ListView) view).getFirstVisiblePosition();
            if (firstVisiblePosition > 0 || firstVisiblePosition < 0) {
                return false;
            }
            View topChild = ((ListView) view).getChildAt(0);
            return topChild.getTop() >= 0;
        }

        if (view instanceof RecyclerView) {
            //显示区域最上面一条信息的position
            RecyclerView.LayoutManager manager = ((RecyclerView) view).getLayoutManager();
            if (manager == null
                    || !(manager instanceof LinearLayoutManager)) {
                return true;
            }
            int visibleItemPosition = ((LinearLayoutManager) manager).findFirstVisibleItemPosition();
            View topChild = getChildAt(0);//getChildAt(0)只能获得当前能看到的item的第一个
            return topChild != null && visibleItemPosition <= 0 && topChild.getTop() >= 0;
        }

        if (view instanceof ScrollView) {
            ScrollView scrollView = ((ScrollView) view);
            int childCount = scrollView.getChildCount();
            if (childCount == 0) {
                return true;
            }
            return scrollView.getScrollY() <= 0;
        }


        if ((view instanceof ViewGroup)) {
            return true;
        }

        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int childCount = getChildCount();
        if (childCount == 0) {
            return super.onTouchEvent(event);
        }

        int action = event.getActionMasked();

        if (action == MotionEvent.ACTION_MOVE) {
            if (shouldAbortDragHelper) {
                event.setAction(MotionEvent.ACTION_CANCEL);
                mDragHelper.processTouchEvent(event);

                MotionEvent actionDown = MotionEvent.obtain(event);
                actionDown.setAction(MotionEvent.ACTION_DOWN); //事件重启

                super.dispatchTouchEvent(actionDown);
                return true;
            }
        }

        mDragHelper.processTouchEvent(event);
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int width = 0;
        if (widthMode == MeasureSpec.UNSPECIFIED) {
            LayoutParams lp = (LayoutParams) getLayoutParams();
            width = displayMetrics.widthPixels - lp.leftMargin - lp.rightMargin ;
        } else {
            width = MeasureSpec.getSize(widthMeasureSpec);
        }

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int height = 0;
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            LayoutParams lp = (LayoutParams) getLayoutParams();
            height = displayMetrics.heightPixels - lp.topMargin - lp.bottomMargin;
        } else {
            height = MeasureSpec.getSize(heightMeasureSpec);
        }


        int childCount = getChildCount();
        if (childCount == 0) return;

        for (int i=0;i<childCount;i++){

            View child = getChildAt(i);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            int contentWidth = width-lp.leftMargin-lp.rightMargin - getPaddingRight() - getPaddingLeft();
            int childHeight = child.getMeasuredHeight();

            if ((child instanceof ListView)
                    || (child instanceof RecyclerView)
                    || (child instanceof ScrollView)) {
                child.setOnTouchListener(this);
                int expectHeight = 0;
                int startIndex = allowHeaderOverScoll?1:0;
                int usedHeight = 0;
                for (int j=i-1;j>=startIndex;j--){
                    View last = getChildAt(j);
                    LayoutParams lastlp = (LayoutParams) last.getLayoutParams();
                    usedHeight +=  last.getMeasuredHeight() +lastlp.bottomMargin+lastlp.topMargin;
                }
                LayoutParams childlp = (LayoutParams) child.getLayoutParams();
                expectHeight = height - usedHeight-childlp.topMargin - childlp.bottomMargin;
                child.measure(MeasureSpec.makeMeasureSpec(contentWidth,MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec(expectHeight,MeasureSpec.EXACTLY));
            }else{
                child.measure(MeasureSpec.makeMeasureSpec(contentWidth,MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec(childHeight,MeasureSpec.EXACTLY));
            }
        }


        setMeasuredDimension(width, height);

    }



    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

      int childCount = getChildCount();
        if (childCount == 0) return;

        int parentHeight = bottom - top;
        int maxOffsetTop = (int) (parentHeight - getChildAt(0).getMeasuredHeight() - dp2px(20) - getPaddingTop());

        if (defaultOffsetTop > maxOffsetTop) {
            this.defaultOffsetTop = maxOffsetTop;
        }

        int childTop = getPaddingTop();
        int childLeft = 0;
        if(isFirstLayout){
            childTop = defaultOffsetTop;
            isFirstLayout = true;
        }

        final int count = getChildCount();


        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child == null) {
                childTop += 0;
            } else  {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LayoutParams lp =
                        (LayoutParams) child.getLayoutParams();

                childLeft  = getPaddingLeft() +  lp.leftMargin;

                childTop += lp.topMargin;
                child.layout(childLeft, childTop ,
                        childWidth, childTop+childHeight);
                childTop += childHeight + lp.bottomMargin + 0;

            }
        }

    }



    private void onViewPositionChanged(View childView, int left, int top, int dx, int dy) {

        int childCount = getChildCount();
        if (childCount == 0) {
            return;
        }
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child == childView) continue;
            int destTop = child.getTop()+dy;

            if(dy<0) {
                if(destTop<getChildMinTop(child)){
                    dy = getChildMinTop(child) - child.getTop();
                }
                ViewCompat.offsetTopAndBottom(child, dy);
            }else{
                if(destTop>getChildMaxTop(child)) {
                    dy = getChildMaxTop(child) - child.getTop();
                }
                ViewCompat.offsetTopAndBottom(child, dy);
            }
        }


        int firstChildTop = getFirstChildTop();
        int firstChildHeight = getFirstChildHeight();

        fixLossFrame();

        if (dy <=0  && firstChildTop <= 0) {
            if (allowHeaderOverScoll) {
                if (Math.abs(firstChildTop) >= firstChildHeight) {
                    shouldAbortDragHelper = true;
                }
            } else {
                shouldAbortDragHelper = true;
            }
        } else {
            shouldAbortDragHelper = false;
        }
        Log.d("CaptureView", "top=" + top);
    }

    /**
     * DragerHelper无法暴漏Scroller,settle滑动状态下abort时会出现位置偏差,
     * 因此需要修复View联动时处理丢帧问题
     */
    private void fixLossFrame() {

        int childCount = getChildCount();
        int firstChildTop = getFirstChildTop();
        int firstChildHeight = getFirstChildHeight();
        View firstChildView = getChildAt(0);
        LayoutParams lp = (LayoutParams) firstChildView.getLayoutParams();
        int offsetTop = firstChildTop + firstChildHeight + lp.topMargin + lp.bottomMargin;

        for (int i = 1; i < childCount; i++) {
            View child = getChildAt(i);
            lp = (LayoutParams) child.getLayoutParams();
            int childTop = child.getTop();
            int expectTop = offsetTop + lp.topMargin;
            if (childTop != expectTop) {
                ViewCompat.offsetTopAndBottom(child, expectTop - childTop);
            }
            offsetTop += child.getHeight() + lp.topMargin + lp.bottomMargin;
        }


    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {


        if (!shouldAbortDragHelper&&!isAtTop(v)) {
            return false;
        }

        int action = event.getActionMasked();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                childStartEventX = event.getX();
                childStartEventY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                boolean isAtTop = isAtTop(v);
                float cy = event.getY();
                float dy = event.getY() - childStartEventY;
                childStartEventY = cy;
                Log.d("onTouchChildView", "isAtTop=" + isAtTop + ",dy=" + dy);
                if (isAtTop && dy > 0) {
                    event.setAction(MotionEvent.ACTION_CANCEL);

                    MotionEvent actionDown = MotionEvent.obtain(event);
                    actionDown.setAction(MotionEvent.ACTION_DOWN);

                    super.dispatchTouchEvent(actionDown);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_OUTSIDE:
                childStartEventX = 0f;
                childStartEventY = 0f;
                Log.d("onTouchChildView", "up");
                break;
        }
        return false;

    }

    public static class ViewDragHelperCallback extends ViewDragHelper.Callback {

        NestedScrollLayout mScrollLayout;

        public ViewDragHelperCallback(NestedScrollLayout scrollLayout) {
            this.mScrollLayout = scrollLayout;
        }

        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            Log.d("CaptureView", "child=" + child);
            return this.mScrollLayout.shouldCaptureView();
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return this.mScrollLayout.computeCaptureViewOffsetTop(child, top, dy);
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            this.mScrollLayout.onViewPositionChanged(changedView, left, top, dx, dy);
        }


        @Override
        public void onViewDragStateChanged(int state) {
            super.onViewDragStateChanged(state);
            Log.d("DragState", "state=" + state);
            this.mScrollLayout.onViewDragStateChanged(state);
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            this.mScrollLayout.onViewReleased(releasedChild, xvel, yvel);
        }
    }

    private void onViewDragStateChanged(int state) {

    }


    private void onViewReleased(View releasedChild, float xvel, float yvel) {
        if (releasedChild == null) return;

        int firstChildTop = getFirstChildTop();
        int rangY = firstChildTop - defaultOffsetTop;
        if(rangY==0) return;

        mDragHelper.abort();
        if (rangY <= 0 && rangY > -dp2px(120) || rangY > 0) {
            int firstChildMaxTop = getChildMaxTop(getChildAt(0));
            mDragHelper.smoothSlideViewTo(releasedChild, 0, getChildMaxTop(releasedChild) - (firstChildMaxTop - defaultOffsetTop));
        } else {
            mDragHelper.smoothSlideViewTo(releasedChild, 0, getChildMinTop(releasedChild));
        }
        ViewCompat.postInvalidateOnAnimation(this);
        Log.d("onViewReleased", "xvel=" + xvel + "   yvel=" + yvel);
    }

    private int getChildMinTop(View child) {

        int childCount = getChildCount();
        int childTopOffset = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView == child) break;
            LayoutParams lp = (LayoutParams) childView.getLayoutParams();
            childTopOffset += childView.getHeight() + lp.topMargin + lp.bottomMargin;

        }
        if (allowHeaderOverScoll) {
            return childTopOffset - getFirstChildHeight();
        } else {
            return childTopOffset;
        }
    }


    private int getChildMaxTop(View child) {

        int childCount = getChildCount();
        int childTopOffset = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView == child) break;
            LayoutParams lp = (LayoutParams) childView.getLayoutParams();
            childTopOffset += childView.getHeight() + lp.topMargin + lp.bottomMargin;

        }
        return childTopOffset + (getHeight() - getFirstChildHeight());
    }


    public int getFirstChildHeight() {
        int childCount = getChildCount();
        if (childCount == 0) return 0;
        return getChildAt(0).getHeight();
    }

    public int getFirstChildTop() {
        int childCount = getChildCount();
        if (childCount == 0) return 0;
        return getChildAt(0).getTop();
    }


    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mDragHelper.continueSettling(true)) {
            if (shouldAbortDragHelper) {
                mDragHelper.abort();
                return;
            }
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    /*
     * 计算view可偏移的top数据
     */
    private int computeCaptureViewOffsetTop(View captureView, int top, int dy) {

        int childCount = getChildCount();
        if (childCount == 0) {
            return 0;
        }
        int childTop = getFirstChildTop();
        int childHeight = getFirstChildHeight();

        if (childTop <= 0 && dy < 0) {
            //处于顶部
            if (allowHeaderOverScoll) {
                if (Math.abs(childTop) >= childHeight) {
                    return captureView.getTop();

                } else if (Math.abs(dy + childTop) > childHeight) {
                    int offset = -(childHeight + childTop);
                    return captureView.getTop() + offset;
                }
            } else {
                if (childTop <= 0) {
                    return getChildMinTop(captureView);
                }
            }
        }

        if (childTop > 0 && dy > 0 && (childTop + childHeight + dy) > getHeight()) {
            //处于底部
            int offset = getHeight() - childTop - childHeight;
            return captureView.getTop() + offset;
        }

        return top;

    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mDragHelper.abort();

    }
}

三、使用

这里我们主要需要注意的是BODY和HEAD的属性标记

java 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/img_map"
    tools:context="com.smartian.widget.layout.ScollLayoutActivity">


    <com.smartian.widget.layout.NestedScrollLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:focusable="true"
        android:focusableInTouchMode="true">


        <TextView
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="@color/colorPrimary"
            android:gravity="center"
            android:text="健康快乐每一天"
            android:textColor="@android:color/white"
            android:textSize="30sp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/colorAccent"
            android:orientation="horizontal">

            <TextView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:gravity="center"
                android:text="Tab A"
                android:textColor="@color/selector_tab" />

            <View
                android:layout_width="1dp"
                android:layout_height="match_parent"
                android:background="#ffffff" />

            <TextView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:gravity="center"
                android:text="Tab B" />
        </LinearLayout>

        <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ffffff" />

    </com.smartian.widget.layout.NestedScrollLayout>

</LinearLayout>

四、总结

主要难度是事件处理和ViewDragHelper多个View联动处理,因此在这里切记,ViewDragHelper适合单个不可滑动的View操作,多个View或者可滑动View建议使用ScorllLing机制。

相关推荐
JUNAI_Strive_ving7 分钟前
力扣6~10题
算法·leetcode·职场和发展
洛临_27 分钟前
【C语言】基础篇续
c语言·算法
戊子仲秋27 分钟前
【LeetCode】每日一题 2024_10_7 最低加油次数(堆、贪心)
算法·leetcode·职场和发展
风清扬_jd35 分钟前
Chromium 硬件加速开关c++
java·前端·c++
谢尔登1 小时前
【React】事件机制
前端·javascript·react.js
osir.2 小时前
Tarjan
算法·图论·tarjan
绎岚科技2 小时前
深度学习中的结构化概率模型 - 从图模型中采样篇
人工智能·深度学习·算法·机器学习
2401_857622662 小时前
SpringBoot精华:打造高效美容院管理系统
java·前端·spring boot
福大大架构师每日一题2 小时前
文心一言 VS 讯飞星火 VS chatgpt (364)-- 算法导论24.3 6题
算法·文心一言
彭于晏6892 小时前
Android高级控件
android·java·android-studio