手记

基于RecyclerView addView时思想改变子View宽高时局部刷新界面

忽然发现很多新技术都没弄过,RecyclerView居然也没用过,于是乎就学习了一下RecyclerView的用法顺带大概看了一下RecyclerView源码,在看到RecyclerView 滑动时调用LayoutManager类的layoutChunk函数addView的时候,我好奇的看了一下,这货为什么滑动中addView不卡,于是我发现了核心。

对了,今天中秋节,先祝大家今天中秋节快乐!
1. 老规矩--先上效果图

未点击按钮时

点击按钮时改变view高度

未使用局部刷新的Log 以及 性能效果图

使用了局部刷新的Log 以及 性能效果图

局部刷新的效果居然这么明显!!!震惊了有木有

2. 来自于RecyclerView的原理

RecyclerView addView调用的时候ViewGroup addView的源码,源码如下(如无特殊说明,以下源码均为api 25)

  public void addView(View child, int index, LayoutParams params) {
        if (DBG) {
            System.out.println(this + " addView");
        }

        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }

        // addViewInner() will call child.requestLayout() when setting the new LayoutParams
        // therefore, we call requestLayout() on ourselves before, so that the child's request
        // will be blocked at our level
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }

也就是会调用requestLayout,这是一个全局刷新的函数,也就是说整个界面将会被刷新,有全局刷新在View层级较多较复杂的时必然存在卡顿
那么RecyclerView 如何做到滑动时addView时不卡的呢,也就是说RecyclerView addView时候为什么没有引起RecyclerView 的onMeasure触发呢,答案就是RecyclerView 以下代码

@Override
    public void requestLayout() {
        if (mEatRequestLayout == 0 && !mLayoutFrozen) {
            super.requestLayout();
        } else {
            mLayoutRequestEaten = true;
        }
    }

尼玛还有这种操作??!复写requestLayout不向上报告,自己做内部处理,内部处理详见LayoutManager类的layoutChunk函数这里不细说。

好了,得到了黑科技的样本,接下来我就来实现一个黑科技的demo,改变View宽高时局部刷新界面。

3. 黑科技的应用

MainActivity布局如下

<?xml version="1.0" encoding="utf-8"?>
<com.zjw.appmethodtime.MyRelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.zjw.appmethodtime.MainActivity">

    <com.zjw.appmethodtime.MyLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.zjw.appmethodtime.MyTextView
            android:id="@+id/text_view"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/colorPrimary"
            android:text="Click Me"
            android:gravity="center"
            android:textSize="25sp"
            android:textStyle="bold"/>
    </com.zjw.appmethodtime.MyLayout>
</com.zjw.appmethodtime.MyRelativeLayout>

自定义一个RelativeLayout 用以看是否局部刷新是否生效,如果局部刷新无效则顶层onMeasure会调用(因为改变了控件大小嘛全局刷新肯定会调用到处于顶层的onMeasure)

public class MyRelativeLayout extends RelativeLayout {
    public MyRelativeLayout(Context context) {
        super(context);
    }

    public MyRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

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

自定义一个MyLayout 继承自LinearLayout ,这里只是demo你想用什么ViewGroup可以自己改。
这里MyLayout 就类似于RecyclerView 了,该子View宽高改变时会调用requestLayout,MyLayout 这里做拦截,然后自行处理。

public class MyLayout extends LinearLayout {
    private int mWidthMeasureSpec;
    private int mheightMeasureSpec;
    private int mLeft;
    private int mTop;
    private int mRight;
    private int mBottom;

    public static boolean shouldLocalIinvalidate = false;

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

    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mLeft = l;
        mTop = t;
        mRight = r;
        mBottom = b;
        super.onLayout(changed, l, t, r, b);
    }

    @Override
    public void requestLayout() {
        if (shouldLocalIinvalidate) {
            localRequestLayout();
        } else {
            super.requestLayout();
        }
    }

    @SuppressLint("WrongCall")
    void localRequestLayout() {
        onMeasure(mWidthMeasureSpec, mheightMeasureSpec);
        onLayout(true, mLeft, mTop, mRight, mBottom);
        invalidate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidthMeasureSpec = widthMeasureSpec;
        mheightMeasureSpec = widthMeasureSpec;
    }
}

以下代码就是在MainActivity里使用改变子View宽高局部刷新界面

package com.zjw.appmethodtime;

import android.content.res.Resources;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.TypedValue;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    protected MyRecycleView mListView;
    protected TextView mTextView;
    private float value;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        super.setContentView(R.layout.activity_main);
        initView();
    }

    private void initView() {
        Resources resources = this.getResources();
        value = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, resources.getDisplayMetrics());
        mTextView = (TextView) findViewById(R.id.text_view);
        mTextView.setOnClickListener(MainActivity.this);
    }

    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.text_view) {
            view.getLayoutParams().height += value;
            //shouldLocalIinvalidate 为true 表示开启局部刷新 否则为关闭(MyLayout shouldLocalIinvalidate 默认为false)
            ((MyLayout) view.getParent()).shouldLocalIinvalidate = true;
            view.requestLayout();
            view.invalidate();
            //局部刷新完成及时恢复成可以全局刷新的状态
            ((MyLayout) view.getParent()).shouldLocalIinvalidate = false;
        }

    }
}

上面MainActivity 的onClick代码中开启了局部刷新(log代码自己加),效果图参见上文。
把上面的((MyLayout) view.getParent()).shouldLocalIinvalidate = true;这句去掉,这就是相当于不启用局部刷新,然后看MyRelativeLayout 的onMeasure方法log(log代码自己加),不启用局部刷新效果图见上文。

3. 应用场景

使用于某个ViewGroup宽高已定位置已定,该子view想改变宽高等场景。

2人推荐
随时随地看视频
慕课网APP