Android 侧滑布局逻辑解析

一、前言

测滑布局应用非常广泛,HorizontalScrollView 本身实现的滑动效果让实现变得很简单,实际上有很多种方式实现,有很多现有的方法可以直接调用。

二、逻辑实现

在Android 中,滑动分为2类,一类以ScrollView为代表布局,通过子View实现布局超出视区(ViewPort)之后,进行Scroll操作的,另一类事以修改Offset为代表的Recycler类,前者实时保持最大高度。形像的理解为前者是"齿轮传动派",后者是"滑板派",两派都有过出分头的时候,即便是个派弟子如NestedScrollView和RecyclerView争的你死我活,不过总体上齿轮传动派占在弱势地位。不过android的改版,让他们做了很多和平相处的事情,不如NestedScrolling机制的支持,让他们想传动就传动,想滑翔就滑翔。

齿轮传动派看家本领

  • scrollX,ScrollY,scrollTo等方法
  • 一个长得很长的独生子

滑板派的看家本领

  • offsetXXX方法

  • 被魔改的ScrollXXX

  • 一群会滑板的孩子

  • layout 方法也是他们的榜首

前者为了实现的简单的滑动,后者空间可以无限大,期间还可自由换孩子。

三、代码实现

有很多现成的example都是基于齿轮传动派的,但是如果使用,你得记住,齿轮传动派会的滑板派一定会,反过来就不一样了。

这里我们使用layout方法实现,核心代码

ini 复制代码
        View leftView = mWrapperView.getChildAt(0);
        leftView.layout(l,t,l+leftView.getWidth(),t+leftView.getHeight());
        maskAlpha = leftView.getLeft()*1.0f/leftView.getWidth();

之所以使用layout的原因是很多人都不记得ListView可以使用该方法实现吸顶效果,而RecyclerView因为为了兼容更多类型,导致他使用这个很难实现吸顶,但是没关系,child.layout和child.measure方法可以在类的任何地方调用,这个是必须要掌握的。

3.1 布局初始化

java 复制代码
 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if(isFirstLayout && getRealChildCount()==2){
            View leftView = mWrapperView.getChildAt(0);
            scrollTo(leftView.getWidth(),0);  //初始化状态让右侧View展示处理
        }
        isFirstLayout = true;
    }

3.2 相对运动

滑动时让左侧View保持同样的滑动距离和方向

ini 复制代码
   View leftView = mWrapperView.getChildAt(0);
        leftView.layout(l,t,l+leftView.getWidth(),t+leftView.getHeight());
        maskAlpha = leftView.getLeft()*1.0f/leftView.getWidth();

3.3 全部代码

ini 复制代码
public class SlidingFoldLayout extends HorizontalScrollView {


    private TextPaint mPaint = null;
    private LinearLayout mWrapperView = null;
    private boolean isFirstLayout = true;
    private float maskAlpha = 1.0f;

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

    public SlidingFoldLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SlidingFoldLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        LinearLayout linearLayout = getWrapperLayout(context);
        setOverScrollMode(View.OVER_SCROLL_NEVER);
        setWillNotDraw(false);
        mPaint = createPaint();
        addViewInLayout(linearLayout, 0, linearLayout.getLayoutParams(), true);
        mWrapperView = linearLayout;
    }


    public LinearLayout getWrapperLayout(Context context) {
        LinearLayout linearLayout = new LinearLayout(context);
        HorizontalScrollView.LayoutParams lp = generateDefaultLayoutParams();
        lp.width = LayoutParams.WRAP_CONTENT;
        linearLayout.setLayoutParams(lp);
        linearLayout.setOrientation(LinearLayout.HORIZONTAL);
        linearLayout.setPadding(0, 0, 0, 0);
        return linearLayout;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = mWrapperView.getChildCount();
        if (childCount == 0) {
            return;
        }
        int leftMenuWidth = mWrapperView.getChildAt(0).getMeasuredWidth();
        ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) getLayoutParams();
        int width = getMeasuredWidth() - getPaddingRight() - getPaddingLeft();
        if (lp instanceof ViewGroup.MarginLayoutParams) {
            width = width - ((MarginLayoutParams) lp).leftMargin - ((MarginLayoutParams) lp).rightMargin;
        }
        if (width <= leftMenuWidth) {
            mWrapperView.getChildAt(0).getLayoutParams().width = (int) (width - dp2px(50));
            measureChild(mWrapperView, widthMeasureSpec, heightMeasureSpec);
        }
        if (childCount != 2) {
            return;
        }
        View rightView = mWrapperView.getChildAt(1);
        int rightMenuWidth = rightView.getMeasuredWidth();
        if (width != rightMenuWidth) {
            rightView.getLayoutParams().width = width;
            measureChild(mWrapperView, widthMeasureSpec, heightMeasureSpec);
            rightView.bringToFront();
        }
    }

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

    @Override
    public void addView(View child) {

        int childCount = mWrapperView.getChildCount();
        if (childCount > 2) {
            throw new IllegalStateException("SlidingFoldLayout should host only two child");
        }
        ViewGroup.LayoutParams lp = child.getLayoutParams();
        if (lp != null && lp instanceof LinearLayout.LayoutParams) {
            lp = new LinearLayout.LayoutParams(lp);
            child.setLayoutParams(lp);
        }

        mWrapperView.addView(child);

    }



    public int getRealChildCount() {
        if (mWrapperView == null) {
            return 0;
        }
        return mWrapperView.getChildCount();
    }



    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if(isFirstLayout && getRealChildCount()==2){
            View leftView = mWrapperView.getChildAt(0);
            scrollTo(leftView.getWidth(),0);
        }
        isFirstLayout = true;
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        int realCount = getRealChildCount();
        if(realCount!=2) return;
        View leftView = mWrapperView.getChildAt(0);
        leftView.layout(l,t,l+leftView.getWidth(),t+leftView.getHeight());
        maskAlpha = leftView.getLeft()*1.0f/leftView.getWidth();
    }


    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();

        switch (action){
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_OUTSIDE:
                super.onTouchEvent(ev);
                scrollToTraget();
                break;
        }

        return super.onTouchEvent(ev);
    }

    private void scrollToTraget() {

        int count = getRealChildCount();
        if(count!=2) return;
        int with = getWidth();
        if(with==0) return;

        View leftView = mWrapperView.getChildAt(0);

        float x = leftView.getLeft()*1.0f/leftView.getWidth();
        if(x > 0.5f){
            smoothScrollTo(leftView.getWidth(),0);
        }else{
            smoothScrollTo(0,0);
        }

    }

    @Override
    public void addView(View child, int index) {

        int childCount = mWrapperView.getChildCount();
        if (childCount > 2) {
            throw new IllegalStateException("SlidingFoldLayout should host only two child");
        }
        ViewGroup.LayoutParams lp = child.getLayoutParams();
        if (lp != null && lp instanceof LinearLayout.LayoutParams) {
            lp = new LinearLayout.LayoutParams(lp);
            child.setLayoutParams(lp);
        }

        mWrapperView.addView(child, index);
    }

    @Override
    public void addView(View child, ViewGroup.LayoutParams params) {
        int childCount = mWrapperView.getChildCount();
        if (childCount > 2) {
            throw new IllegalStateException("SlidingFoldLayout should host only two child");
        }
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(params);
        child.setLayoutParams(lp);
        mWrapperView.addView(child, lp);

    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        int childCount = mWrapperView.getChildCount();
        if (childCount > 2) {
            throw new IllegalStateException("SlidingFoldLayout should host only two child");
        }

        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(params);
        child.setLayoutParams(lp);
        mWrapperView.addView(child, index);
    }

    private TextPaint createPaint() {
        // 实例化画笔并打开抗锯齿
        TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        paint.setAntiAlias(true);
        return paint;
    }
  RectF rectF = new RectF();
    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);

        int realCount = getRealChildCount();
        if(realCount!=2) return;
        View leftView = mWrapperView.getChildAt(0);
        View rightView = mWrapperView.getChildAt(1);

      
        rectF.top = leftView.getTop();
        rectF.bottom = leftView.getBottom();
        rectF.left = leftView.getLeft();
        rectF.right = rightView.getLeft();
        int alpha = (int) (153*maskAlpha);
        mPaint.setColor(argb(alpha,0x00,0x00,0x00));
        int saveId = canvas.save();
        canvas.drawRect(rectF,mPaint);
        canvas.restoreToCount(saveId);
    }

    public static int argb(
             int alpha,
             int red,
             int green,
             int blue) {
        return (alpha << 24) | (red << 16) | (green << 8) | blue;
    }

}

三、使用方式

使用方式简单清晰,没有看到ScrollView的独生子,原因是我们把他写到了类里面

ini 复制代码
  <com.cn.scrolllayout.view.SlidingFoldLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="300dp"
        android:layout_height="match_parent"
        android:gravity="center"
        >
      <ImageView
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:scaleType="centerCrop"
          android:src="@mipmap/img_sample_text"
          />
    </LinearLayout>
    <LinearLayout
        android:layout_width="500dp"
        android:layout_height="match_parent"
        android:background="@color/colorAccent"
        >
      <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitCenter"
        android:src="@mipmap/img_sample_panda"
        />
    </LinearLayout>
  </com.cn.scrolllayout.view.SlidingFoldLayout>

四、总结

掌握ScrollX和OffsetX两种的滑动很重要,但是不能忘记layout的作用,本质上他属于一种OffsetX上层的封装。

相关推荐
Jasmin Tin Wei6 分钟前
计算机操作系统(计算题公式)
前端
小盐巴小严10 分钟前
浏览器基础及缓存
前端·缓存
Hilaku11 分钟前
🔥这 10 个 Vue3 性能优化技巧,藏太深了,建议保存!
前端·javascript·vue.js
FogLetter15 分钟前
你不知道的Javascript(上卷) | 第一章:作用域是什么
前端·javascript·编程语言
_一条咸鱼_18 分钟前
Android Runtime增量编译与差分更新机制原理(45)
android·面试·android jetpack
刃神太酷啦20 分钟前
聚焦 string:C++ 文本处理的核心利器--《Hello C++ Wrold!》(10)--(C/C++)
java·c语言·c++·qt·算法·leetcode·github
CoovallyAIHub21 分钟前
云南电网实战:YOLOv8m改进模型攻克输电线路异物检测难题技术详解
深度学习·算法·计算机视觉
一只猫猫熊21 分钟前
Vue实战:手把手教你封装一个可拖拽并支持穿透操作的弹窗组件
前端·vue.js
pixle022 分钟前
前端 EventSource(SSE)实时通信使用指南(EventSource-polyfill)
前端·web·sse·eventsource·polyfill
yma1625 分钟前
react_flow自定义节点、边——使用darg布局树状结构
前端·react.js·前端框架·reac_flow