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上层的封装。

相关推荐
雷特IT17 分钟前
Uncaught TypeError: 0 is not a function的解决方法
前端·javascript
追光天使36 分钟前
【Mac】和【安卓手机】 通过有线方式实现投屏
android·macos·智能手机·投屏·有线
长路 ㅤ   39 分钟前
vite学习教程02、vite+vue2配置环境变量
前端·vite·环境变量·跨环境配置
睡不着还睡不醒39 分钟前
【数据结构强化】应用题打卡
算法
sp_fyf_20241 小时前
计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-05
人工智能·深度学习·神经网络·算法·机器学习·语言模型·自然语言处理
亚里士多没有德7751 小时前
强制删除了windows自带的edge浏览器,重装不了怎么办【已解决】
前端·edge
micro2010141 小时前
Microsoft Edge 离线安装包制作或获取方法和下载地址分享
前端·edge
.生产的驴1 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
awonw1 小时前
[前端][easyui]easyui select 默认值
前端·javascript·easyui
小雨cc5566ru1 小时前
uniapp+Android智慧居家养老服务平台 0fjae微信小程序
android·微信小程序·uni-app