Android-RecyclerView详细学习

2017/6/25 posted in  Android

RecyclerView

RecyclerView是什么?

RecylerView是support-v7包中的新组件,是一个强大的滑动组件,与经典的ListView相比,同样拥有item回收复用的功能,这一点从它的名字recylerview即回收view也可以看出。看到这也许有人会问,不是已经有ListView了吗,为什么还要RecylerView呢?这就牵扯到第二个问题了。

RecyclerView的优点是什么?

根据官方的介绍RecylerView是ListView的升级版,既然如此那RecylerView必然有它的优点,现就RecylerView相对于ListView的优点罗列如下:

  • RecylerView封装了viewholder的回收复用,也就是说RecylerView标准化了ViewHolder,编写Adapter面向的是ViewHolder而不再是View了,复用的   逻辑被封装了,写起来更加简单。
  • 提供了一种插拔式的体验,高度的解耦,异常的灵活,针对一个Item的显示RecylerView专门抽取出了相应的类,来控制Item的显示,使其的扩展性非常强。例如:你想控制横向或者纵向滑动列表效果可以通过LinearLayoutManager这个类来进行控制(与GridView效果对应的是GridLayoutManager,与瀑布流对应的还有StaggeredGridLayoutManager等),也就是说RecylerView不再拘泥于ListView的线性展示方式,它也可以实现GridView的效果等多种效果。你想控制Item的分隔线,可以通过继承RecylerView的ItemDecoration这个类,然后针对自己的业务需求去抒写代码。
  • 可以控制Item增删的动画,可以通过ItemAnimator这个类进行控制,当然针对增删的动画,RecylerView有其自己默认的实现。

RecyclerView的用法

RecyclerView的初步用法(包括RecyclerView.Adapter用法)

recyclerView = (RecyclerView) findViewById(R.id.recyclerView);  
LinearLayoutManager layoutManager = new LinearLayoutManager(this );  
//设置布局管理器  
recyclerView.setLayoutManager(layoutManager);  
//设置为垂直布局,这也是默认的  
layoutManager.setOrientation(OrientationHelper. VERTICAL);  
//设置Adapter  
recyclerView.setAdapter( recycleAdapter);  
//设置分隔线  
recyclerView.addItemDecoration( new DividerGridItemDecoration(this ));  
//设置增加或删除条目的动画  
recyclerView.setItemAnimator( new DefaultItemAnimator());

可以看到对RecylerView的设置过程,比ListView要复杂一些,这也是RecylerView高度解耦的表现,虽然代码抒写上有点复杂,但它的扩展性是极高的。

RecyclerView的生命周期

一个RecyclerView的Item加载是有顺序的,类似于Activity的生命周期(姑且这么叫把),具体可以对adapter的每个方法进行重写打下日志进行查看,具体大致为:

  • getItemViewType(获取显示类型,返回值可在onCreateViewHolder中拿到,以决定加载哪种ViewHolder)
  • onCreateViewHolder(加载ViewHolder的布局)
  • onViewAttachedToWindow(当Item进入这个页面的时候调用)
  • onBindViewHolder(将数据绑定到布局上,以及一些逻辑的控制就写这啦)
  • onViewDetachedFromWindow(当Item离开这个页面的时候调用)
  • onViewRecycled(当Item被回收的时候调用)
    tips1:如果你调用了:
viewHolder.setIsRecyclable(false);

那么这个Item的onViewRecycled将永远不会调用。
tips2:如果你的界面出现了错乱的现象,请调用如上代码可能能简单粗暴的解决,当然代价是损失少许的性能表现了。

RecyclerView.Adapter

来看看它的Adapter的写法,RecyclerView的Adapter与ListView的Adapter还是有点区别的,RecyclerView.Adapter,需要实现3个方法:

  • onCreateViewHolder()这个方法主要生成为每个Item inflater出一个View,但是该方法返回的是一个ViewHolder。该方法把View直接封装在ViewHolder中,然后我们面向的是ViewHolder这个实例,当然这个ViewHolder需要我们自己去编写。直接省去了当初的convertView.setTag(holder)和convertView.getTag()这些繁琐的步骤。
  • onBindViewHolder()这个方法主要用于适配渲染数据到View中。方法提供给你了一个viewHolder,而不是原来的convertView。
  • getItemCount()这个方法就类似于BaseAdapter的getCount方法了,即总共有多少个条目。

  • notifyDataSetChanged()刷新所有,notifyDataSetChanged最终会使adapter的数据重新绑定,即会重新调用adapter里的onBindViewHolder方法,从而使item的position得到了更新。

  • notifyItemChanged(int position)position数据发生了改变,那调用这个方法,就会回调对应position的onBindViewHolder()方法了

  • notifyItemRangeChanged(int positionStart, int itemCount)刷新从positionStart开始itemCount数量的item了(这里的刷新指回调onBindViewHolder()方法)

  • notifyItemInserted(int position)在第position位置被插入了一条数据的时候可以使用这个方法刷新,注意这个方法调用后会有插入的动画,这个动画可以使用默认的,也可以自己定义

  • notifyItemMoved(int fromPosition, int toPosition)从fromPosition移动到toPosition为止的时候可以使用这个方法刷新

  • notifyItemRangeInserted(int positionStart, int itemCount)批量添加

  • notifyItemRemoved(int position)第position个被删除的时候刷新,同样会有动画

  • notifyItemRangeRemoved(int positionStart, int itemCount)批量删除

  • onAttachedToRecyclerView (RecyclerView recyclerView)RecyclerView.setAdapter(adapter)时进行调用

  • onViewAttachedToWindow(VH holder)当Item进入这个页面的时候调用

  • onViewDetachedFromWindow(VH holder)当Item离开这个页面的时候调用

  • onViewRecycled(VH holder)当Item被回收的时候调用

RecyclerView.ItemDecoration

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.

ItemDecoration允许应用结合adapter的数据集,对特定的item添加绘制一个周边图案。可以用于给items之间添加分割线、高亮装饰效果或者分组边界等等。

从谷歌官方的介绍可以知道,ItemDecoration是用于给列表的item添加各种装饰效果,开发中最常见的就是为item添加分割线。

ItemDecoration本身是一个抽象类,抛去废弃的方法,我们需要关心的方法只有三个:

public static abstract class ItemDecoration {
    public void onDraw(Canvas c, RecyclerView parent, State state) {
        onDraw(c, parent);
    }
    public void onDrawOver(Canvas c, RecyclerView parent, State state) {
        onDrawOver(c, parent);
    }
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
        getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),parent);
    }
}

从源码注释中,可以大概了解这三个方法的用途:

  • onDraw:在item绘制之前时被调用,将指定的内容绘制到item view内容之下;
  • onDrawOver:在item被绘制之后调用,将指定的内容绘制到item view内容之上
  • getItemOffsets:在每次测量item尺寸时被调用,将decoration的尺寸计算到item的尺寸中

20171015150799957749046.png
20171015150799957749046.png

ItemDecoration三个方法的测试

谷歌官方在support.v7包中提供了ItemDecoration的一个实现DividerItemDecoration,这里结合这个实现,来看看其三个需要实现的方法对UI的影响。

onDraw

private void drawVertical(Canvas canvas, RecyclerView parent) {
    canvas.save();
   final int left;
   final int right;
   if (parent.getClipToPadding()) {
        left = parent.getPaddingLeft();
        right = parent.getWidth() - parent.getPaddingRight();
        canvas.clipRect(left, parent.getPaddingTop(), right,parent.getHeight() - parent.getPaddingBottom());
   } else {
        left = 0;
        right = parent.getWidth();
    }

    final int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = parent.getChildAt(i);
        parent.getDecoratedBoundsWithMargins(child, mBounds);
        final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
        final int top = bottom - mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
       mDivider.draw(canvas);
    }
    canvas.restore();
}

drawVertical方法实现了对Orientation == VERTICAL的RecyclerView绘制item之间的分割线。从传入的canvas参数可以推断,分割线的绘制是通过canvas机制绘制到屏幕上:mDivider.draw(canvas);其中,mDivider是一个Drawable对象,可以通过setDrawable传入自定义对象,不传入时,会自动使用系统内置的分割线样式:android.R.attr.listDivider。通过遍历每一个可见的child view,计算mDivider对应的left、top、right、bottom值,从而绘制到正确的位置上。对于纵向的RecyclerView而言,mDivider的left和right是固定的,和parent的左右内容边界保持一致,也就是说,把parent的左右padding都计算进去,因而是代表了RecyclerView实际的内容区域。纵向的分割线一般位于每个item的底部,因此mDivider的top值理论上应该和child view的内容下边界保持贴合。实际上,计算top和bottom的代码,谷歌官方也有所调整,在最新的实现中,先通过parent.getDecoratedBoundsWithMargins(child, mBounds);拿到之前在onMeasure过程中,通过调用getItemOffsets获取到的mBounds,mBounds是包括了整个child view以及其decoration的总边界,之后再计算mDivider的bottom、top值。

getItemOffsets

public void getItemOffsets(Rect outRect, View view, RecyclerView parent,RecyclerView.State state) {
    if (mOrientation == VERTICAL) {
        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
    } else {
        outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
    }
}

官方实现的getItemOffsets比较简单,只是根据列表的方向,返回了分割线在相应方向的尺寸。这里可能有一个坑,即通过setDrawable设置自定义的分割线时,容易传入一个无尺寸的drawable对象,导致分割线无法显示出来的bug,典型的代码是这样:
decoration.setDrawable(new ColorDrawable(Color.RED));

DividerItemDecoration的实现中,是没有复写onDrawOver方法的,对于分割线场景而言,也确实不需要去实现它。接下来,通过几个例子,展示一下getItemOffsets对于ItemDecoration在UI上的影响。

getItemOffsets & onDraw

先上动图【注2】:

20171015150799983096582.gif
20171015150799983096582.gif

20171015150799985361609.gif
20171015150799985361609.gif

上图中,getItemOffsets方法里,返回outRect不同,而onDraw方法绘制的分割线高度初始值设为25,并通过外部增减来观察其UI效果。

public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    outRect.set(0, 0, 0, 50);// outRect.set(50,50,50,50);
}

public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    for (int i = 0; i < childCount; i++) {
        final View view = parent.getChildAt(i);
        top = view.getBottom();
        left = view.getPaddingLeft() + mSize;
        right = view.getWidth() - view.getPaddingRight() - mSize ;
        bottom = top + mSize;
        divider.setBounds(left, top, right, bottom);
        divider.draw(c);
    }
}

从上面两个动图对比,可以得出以下几个结论:

  • getItemOffsets返回的矩形outRect会被计算到child view的尺寸当中;
  • onDraw方法绘制的图形,可以超出outRect所规定的区域;
  • onDraw方法绘制的图形,确实是处于child view的底下,当两者发生重叠时,只会显示child view的内容;

getItemOffsets & onDrawOver

20171015150799996057841.gif
20171015150799996057841.gif

将之前onDraw方法内代码完整拷贝到onDrawOver下,并注释掉之前onDraw中的方法,很容易验证出onDrawOver与onDraw的唯一不同之处。

  • onDrawOver绘制的图形,处于child view之上,当两者发生重叠时,会显示onDrawOver的内容;

ItemDecoration三个方法的含义,就介绍到这里。可以感觉到,三个方法都很简单而基础,可以十分优雅的实现item的分割线效果,然而简单的如DividerItemDecoration,往往是无法满足项目开发需求的。经常会遇到某几个item不想要分割线(如头部或者最后一个item),这就需要开发者自行来实现。

添加分隔线

我们可以创建一个继承RecyclerView.ItemDecoration类来绘制分隔线,通过ItemDecoration可以让我们每一个Item从视觉上面相互分开来,例如ListView的divider非常相似的效果。也可以不设置ItemDecoration,那说明ItemDecoration我们并不是强制需要使用,作为我们开发者可以设置或者不设置Decoration的。实现一个ItemDecoration,系统提供的ItemDecoration是一个抽象类,内部除去已经废弃的方法以外,我们主要实现以下三个方法:

public static abstract class ItemDecoration {   
    public void onDraw(Canvas c,RecyclerView parent,State state) {   
        onDraw(c,parent);   
    }   
    public void onDrawOver(Canvas c,RecyclerView parent,State state) {   
        onDrawOver(c,parent);   
    }   
    public void getItemOffsets(RectoutRect, View view,RecyclerView parent,State state) {   
        getItemOffsets(outRect,((LayoutParams)view.getLayoutParams()).getViewLayoutPosition(),parent);   
    }   
}
  • onDraw方法先于drawChildren
  • onDrawOverdrawChildren之后,一般我们选择复写其中一个即可。
  • getItemOffsets 可以通过outRect.set()为每个Item设置一定的偏移量,主要用于绘制Decorator

又因为当我们RecyclerView在进行绘制的时候会进行绘制Decoration,那么会去调用onDraw和onDrawOver方法,那么这边我们其实只要去重写onDraw和getItemOffsets这两个方法就可以实现啦。然后LayoutManager会进行Item布局的时候,会去调用getItemOffset方法来计算每个Item的Decoration合适的尺寸,下面我们来具体实现一个

package com.example.reclerviewpractice;  
  
import android.content.Context;  
import android.content.res.TypedArray;  
import android.graphics.Canvas;  
import android.graphics.Rect;  
import android.graphics.drawable.Drawable;  
import android.support.v7.widget.LinearLayoutManager ;  
import android.support.v7.widget.RecyclerView;  
import android.view.View;  
  
public class DividerItemDecoration extends RecyclerView.ItemDecoration {  
  
    private static final int[] ATTRS = new int[]{  
        android.R.attr. listDivider  
    };  
    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;  
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;  
    private Drawable mDivider;
    private int mOrientation;  
  
    public DividerItemDecoration(Context context, int orientation) {  
        final TypedArray a = context.obtainStyledAttributes(ATTRS );  
        mDivider = a.getDrawable(0);  
        a.recycle();  
        setOrientation(orientation);  
    }  
  
    public void setOrientation( int orientation) {  
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {  
            throw new IllegalArgumentException( "invalid orientation");  
        }  
        mOrientation = orientation;  
    }  
  
    @Override  
    public void onDraw(Canvas c, RecyclerView parent) {  
        if (mOrientation == VERTICAL_LIST) {  
            drawVertical(c, parent);  
        } else {  
            drawHorizontal(c, parent);  
        }  
    }  
  
    public void drawVertical(Canvas c, RecyclerView parent) {  
        final int left = parent.getPaddingLeft();  
        final int right = parent.getWidth() - parent.getPaddingRight();  
        final int childCount = parent.getChildCount();  
        for (int i = 0; i < childCount; i++) {  
            final View child = parent.getChildAt(i);  
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();  
            final int top = child.getBottom() + params.bottomMargin;  
            final int bottom = top + mDivider.getIntrinsicHeight();  
            mDivider.setBounds(left, top, right, bottom);  
            mDivider.draw(c);  
        }  
    }  
  
    public void drawHorizontal(Canvas c, RecyclerView parent) {  
        final int top = parent.getPaddingTop();  
        final int bottom = parent.getHeight() - parent.getPaddingBottom();  
  
        final int childCount = parent.getChildCount();  
        for (int i = 0; i < childCount; i++) {  
            final View child = parent.getChildAt(i);  
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();  
            final int left = child.getRight() + params.rightMargin;  
            final int right = left + mDivider.getIntrinsicHeight();  
            mDivider.setBounds(left, top, right, bottom);  
            mDivider.draw(c);  
        }  
    }  
  
    @Override  
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {  
        if (mOrientation == VERTICAL_LIST) {  
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());  
        }else{  
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);  
        }  
    }  
}

改变分隔线样式

那么怎么更改分隔线的样式呢?在上面的DividerItemDecoration这个类中可以看到这个分隔线是跟ListView一样的,即系统的默认的样式,因此我们可以在styles的xml文件中进行更改,更改如下:

<!-- Application theme. -->  
<style name ="AppTheme" parent="AppBaseTheme">  
    <!-- All customizations that are NOT specific to a particular API-level can go here. -->  
    <item name= "android:listDivider">@drawable/divider </item >   
</style >

divider的内容如下:

<?xml version="1.0" encoding= "utf-8"?>  
<shape xmlns:android="http://schemas.android.com/apk/res/android"  
    android:shape="rectangle" >  
     
    <!-- 填充的颜色 -->  
    <solid android:color ="@color/color_red"/>  
       
    <!--  线条大小 -->  
    <size android:height ="1dp" android:width ="1dp"/>  
</shape>

看到这肯定会有人说,这尼玛,好麻烦,还不如ListView简单呢,从上面的代码量看来确实是使用起来很复杂,但是如果此时你想将这个列表以GridView的形式展示出来,用RecylerView仅仅是换一行代码的事情.

简单的封装MKItemDecoration

  • 支持简单颜色分割线
  • 支持简单颜色分割线 + 文字:文字可以居左、居中
  • 支持分割线跳过起始诺干个item,跳过最后一个item
  • 支持分组悬停效果
  • 支持自定义View作为Decoration

2017101515080000957381.gif
2017101515080000957381.gif

上图hoverGroup.gif的使用代码如下:

recyclerView.addItemDecoration(new MKItemDecoration.Builder()
.height(50)
.color(Color.parseColor("#525D97"))
.textSize(30)
.textColor(Color.WHITE)
.itemOffset(0)
.iHover(new IHover() {
    @Override
    public boolean isGroup(int position) {
        return position % 4 == 0;
    }

    @Override
    public String groupText(int position) {
        return adapter.data.get(4 * (position / 4));
    }
})
.textAlign(MKItemDecoration.Builder.ALIGN_MIDDLE)
.build());

通过封装,利用builder模式来更好的自定义需要的Decoration,其中,为了支持自定义View,需要外部传入相关的view的资源id和需要绑定的数据List,控件内部会通过view的measure,layout,draw的流程,将其绘制在屏幕上。

具体代码

RecyclerView.ViewHolder

  • getPosition()在API22的时候已经被废弃,因为它在异步处理器更新的时候不能准确表示数据,是模棱两可的。请根据你所使用的场景参照使用getLayoutPosition() 和 getAdapterPosition()

  • getAdapterPosition()在调用notifyDataSetChanged之后并不能马上获取Adapter中的position, 要等布局结束之后才能获取到.在调用notifyItemInserted(0)之后能通过getAdapterPosition()获取适配器位置即使新的布局还没有计算。,如果你做一些用户点击,如果getAdapterPosition()返回NO_POSITION,最好忽略点击,因为你不知道用户点击(除非你有其他机制,如稳定的id查找条目)。

  • getLayoutPosition()假设您正在使用LayoutManager或者想要访问ViewHolder当前点击的项目。在这种情况下,您应该使用getLayoutPosition()来获取当前布局位置。mRecyclerView.findViewHolderForLayoutPosition(myViewHolder.getLayoutPosition() - 1)

RecyclerView.ItemAnimator

  • animateAppearance(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preLayoutInfo, RecyclerView.ItemAnimator.ItemHolderInfo postLayoutInfo)
    当RecyclerView中的item显示到屏幕上时调用此方法。传入的layout之后的ViewHolder对象。

  • animateDisappearance(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preLayoutInfo, RecyclerView.ItemAnimator.ItemHolderInfo postLayoutInfo)
    当RecyclerView中的item在屏幕上由可见变为不可见时调用此方法。传入的layout之后的ViewHolder对象。

  • animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, RecyclerView.ItemAnimator.ItemHolderInfo preLayoutInfo, RecyclerView.ItemAnimator.ItemHolderInfo postLayoutInfo)
    当RecyclerView中的item状态发生改变时调用此方法(notifyItemChanged(position))。
    方法中传入了layout之前的ViewHolder和layout之后的ViewHolder对象,通过这两个ViewHolder对象获取其中的itemView进行动画效果。

  • runPendingAnimations()
    统筹RecyclerView中所有的动画,统一启动执行

  • setRemoveDuration(long removeDuration)/getRemoveDuration()
     设置删除Item动画的延迟时间

  • setMoveDuration(long moveDuration)/getMoveDuration()
    设置移动Item动画的延迟时间

  • setChangeDuration(long changeDuration)/getChangeDuration()
    设置改变Item动画的延迟时间

  • setAddDuration(long addDuration)/getAddDuration()
    设置添加Item动画的延迟时间

  • recordPostLayoutInformation(RecyclerView.State state, RecyclerView.ViewHolder viewHolder)
    布局完成后对这个方法进行调用,记录view的必要信息。

  • onAnimationStarted(RecyclerView.ViewHolder viewHolder)
    当一个新动画添加到这个ViewHolder上,调用此方法

  • onAnimationFinished(RecyclerView.ViewHolder viewHolder)
    dispatchAnimationFinished(ViewHolder)这个方法调用后调用

  • obtainHolderInfo()
    获取ViewHolder保存的RecyclerView.ItemAnimator.ItemHolderInfo信息

  • isRunning()
    判断是否有Item动画在运行

  • endAnimations()
    停止所有动画

  • endAnimation(RecyclerView.ViewHolder item)
    停止指定动画

RecyclerView.LayoutManager

RecyclerView.LayoutManager是一个抽象类,系统为我们提供了三个实现类

  • LinearLayoutManager即线性布局,这个是在上面的例子中我们用到的布局
  • GridLayoutManager即表格布局
  • StaggeredGridLayoutManager即流式布局,如瀑布流效果假如将上述例子换成GridView的效果,那么相应的代码应该这样改

给RecyclerView的Item添加点击事件

ListView给我们提供了onItemClickListener的监听器,但对于RecyclerView来讲,非常可惜的是,该控件没有给我们提供这样的内置监听器方法,不过我们可以进行改造实现,可以这样实现Item的点击事件的监听,在我们的adapter中增加这两个方法

public interface OnItemClickListener{
    void onClick( int position);
    void onLongClick( int position);
}

public void setOnItemClickListener(OnItemClickListener onItemClickListener ){
    this. mOnItemClickListener=onItemClickListener;
}

然后onBindViewHolder方法要做如下更改

@Override  
public void onBindViewHolder(MyViewHolder holder, final int position) {  
    holder. tv.setText( mDatas.get(position));  
    if( mOnItemClickListener!= null){  
        holder. itemView.setOnClickListener( new OnClickListener() {  
        @Override  
        public void onClick(View v) {  
            mOnItemClickListener.onClick(position);  
        }  
    });  
                  
    holder. itemView.setOnLongClickListener( new OnLongClickListener() {  
        @Override  
        public boolean onLongClick(View v) {  
            mOnItemClickListener.onLongClick(position);  
            return false;  
        }  
    });  
}  

RecyclerView局部刷新界面

来自于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宽高时局部刷新界面。

黑科技的应用

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;这句去掉,这就是相当于不启用局部刷新,然后看MyRelativeLayoutonMeasure方法log(log代码自己加),不启用局部刷新效果图见上文。

应用场景

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