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机制。

相关推荐
皮蛋sol周14 分钟前
嵌入式学习C语言(八)二维数组及排序算法
c语言·学习·算法·排序算法
LuckyLay17 分钟前
使用 Docker 搭建 Rust Web 应用开发环境——AI教你学Docker
前端·docker·rust
pobu16836 分钟前
aksk前端签名实现
java·前端·javascript
森焱森38 分钟前
单片机中 main() 函数无 while 循环的后果及应对策略
c语言·单片机·算法·架构·无人机
烛阴41 分钟前
带参数的Python装饰器原来这么简单,5分钟彻底掌握!
前端·python
0wioiw01 小时前
Flutter基础(前端教程⑤-组件重叠)
开发语言·前端·javascript
人生游戏牛马NPC1号1 小时前
学习 Flutter (一)
android·学习·flutter
平和男人杨争争1 小时前
机器学习12——支持向量机中
算法·机器学习·支持向量机
冰天糖葫芦1 小时前
VUE实现数字翻牌效果
前端·javascript·vue.js
南岸月明1 小时前
我与技术无缘,只想副业搞钱
前端