使用 DPAD 快速滚动时,RecyclerView onCreateViewHolder 调用过多

RecyclerView onCreateViewHolder called excessively when scrolling fast with DPAD

我正在 Amazon Fire TV 上开发。

因为它是电视应用(无触摸),所以我需要行布局内的可聚焦对象才能四处导航。

我有一个非常简单的 Recyclerview,带有图像、文本和可聚焦的。当我向上或向下按时,它全部滚动并正确填充,但我注意到当我导航速度快于滚动速度时,它会创建新的 viewholders(屏幕外)并滞后于 UI.

我创建了一个 activity,上面有创建编号。当我慢慢滚动时,最高的创作#是10。但是当我快速滚动时,我会在一秒钟内得到创作编号为60的卡片。这会导致巨大的延迟,并且应用程序会丢掉很多帧。我的做法完全错误吗?

使用下面的代码对此进行测试。

/**
 * Created by sylversphere on 15-04-15.
 */
public class LandfillActivity extends Activity{

private Context context;

private static int ticketNumber;
private static int getTicket(){
    ticketNumber ++;
    return ticketNumber;
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    context = this;
    setContentView(R.layout.landfill_activity);
    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
    GridLayoutManager glm = new GridLayoutManager(context, 2);
    recyclerView.setLayoutManager(glm);
    SickAdapter sickAdapter = new SickAdapter();
    recyclerView.setAdapter(sickAdapter);
}

public class SickViewHolder extends RecyclerView.ViewHolder{
    TextView ticketDisplayer;
    public ImageView imageView;
    public SickViewHolder(View itemView) {
        super(itemView);
        ticketDisplayer = (TextView) itemView.findViewById(R.id.ticketDisplayer);
        imageView = (ImageView) itemView.findViewById(R.id.imageView);

        itemView.findViewById(R.id.focus_glass).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                context.startActivity(new Intent(context, LouisVuittonActivity.class));
            }
        });
    }
    public void setTicket(int value){
        ticketDisplayer.setText(""+value);
    }
}

public class SickAdapter extends RecyclerView.Adapter<SickViewHolder>{

    @Override
    public SickViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        SickViewHolder svh = new SickViewHolder(getLayoutInflater().inflate(R.layout.one_row_element, null));
        svh.setTicket(getTicket());
        return svh;
    }

    @Override
    public void onBindViewHolder(SickViewHolder holder, int position) {
        String[] image_url_array = getResources().getStringArray(R.array.test_image_urls);
        Picasso.with(context).load(image_url_array[position % image_url_array.length] ).fit().centerCrop().into(holder.imageView);
    }

    @Override
    public int getItemCount() {
        return 100000;
    }
}
}

one_row_element.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:orientation="horizontal">
        <ImageView
            android:id="@+id/imageView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:adjustViewBounds="true"
            android:scaleType="centerCrop"
            android:src="@mipmap/sick_view_row_bg" />
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="left|center_vertical"
            android:layout_marginLeft="15dp"
            android:orientation="horizontal">
            <TextView
                android:id="@+id/virusTextView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Creation #"
                android:textColor="#fff"
                android:textSize="40sp" />
            <TextView
                android:id="@+id/ticketDisplayer"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="1"
                android:textColor="#fff"
                android:textSize="40sp" />
        </LinearLayout>
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/focus_glass"
            android:background="@drawable/subtle_focus_glass"
            android:focusable="true"
            android:focusableInTouchMode="true"/>
    </FrameLayout>
</FrameLayout>

test_image_urls.xml(不属于我的网址)

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="test_image_urls"
    formatted="false">
    <item>http://farm4.static.flickr.com/3175/2737866473_7958dc8760.jpg</item>
    <item>http://farm4.static.flickr.com/3276/2875184020_9944005d0d.jpg</item>
    <item>http://farm3.static.flickr.com/2531/4094333885_e8462a8338.jpg</item>
    <item>http://farm4.static.flickr.com/3289/2809605169_8efe2b8f27.jpg</item>
    <item>http://2.bp.blogspot.com/_SrRTF97Kbfo/SUqT9y-qTVI/AAAAAAAABmg/saRXhruwS6M/s400/bARADEI.jpg</item>
    <item>http://fortunaweb.com.ar/wp-content/uploads/2009/10/Caroline-Atkinson-FMI.jpg</item>
    <item>http://farm4.static.flickr.com/3488/4051378654_238ca94313.jpg</item>
    <item>http://farm4.static.flickr.com/3368/3198142470_6eb0be5f32.jpg</item>
    <item>http://www.powercai.net/Photo/UploadPhotos/200503/20050307172201492.jpg</item>
    <item>http://www.web07.cn/uploads/Photo/c101122/12Z3Y54RZ-22027.jpg</item>
    <item>http://www.mitravel.com.tw/html/asia/2011/Palau-4/index_clip_image002_0000.jpg</item>
    <item>http://news.xinhuanet.com/mil/2007-05/19/xinsrc_36205041914150623191153.jpg</item>
    <item>http://ib.berkeley.edu/labs/koehl/images/hannah.jpg</item>
    <item>http://down.tutu001.com/d/file/20110307/ef7937c2b70bfc2da539eea9df_560.jpg</item>
    <item>http://farm3.static.flickr.com/2278/2300491905_5272f77e56.jpg</item>
    <item>http://www.pic35.com/uploads/allimg/100526/1-100526224U1.jpg</item>
    <item>http://img.99118.com/Big2/1024768/20101211/1700013.jpg</item>
    <item>http://farm1.static.flickr.com/45/139488995_bd06578562.jpg</item>
</string-array>
</resources>

subtle_focus

    <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_focused="true" android:drawable="@color/glass_focus"/>
    <item android:drawable="@color/glass_normal"/>
</selector>

glass_normal#9000

glass_focus#0000

正如评论者所指出的,毕加索的未决回复可能会阻碍您的发展。如果是这种情况,您可以通过扩展 ImageView 并覆盖以下方法来解决它。我觉得值得一试。

@Override
protected void onDetachedFromWindow() {
    Picasso.with(context).cancelRequest(this);
    super.onDetachedFromWindow();
}

更新:

事实证明这不是正确的方法,任何想要取消请求的人都应该在 onViewRecycled() 回调中这样做,正如下面评论中指出的那样。

Picasso 阻碍了你,但建立你自己的机制的建议解决方案不是可行的方法。

Picasso 在过去一年左右的时间里落后了,现在有更好的替代方案 Google Glide and Facebook Fresco,它专门发布更新以更好地与 RecyclerView 配合使用,并且事实证明加载速度更快、效率更高,在许多在线可用的测试中缓存和存储,例如:

希望对您有所帮助。 祝你好运。

尝试增加池中的 maximum number of recycled views

recyclerView.getRecycledViewPool().setMaxRecycledViews(50);

50 是一个任意数字,您可以尝试更高或更低,看看会发生什么。

RecyclerView 尽量避免重复使用具有 transient state 的视图,因此如果视图很快失效或正在动画化,它们可能不会立即被重新使用。

同样,如果您有许多较小的视图,您最终可能会在屏幕上看到比默认池大小可以处理的更多的视图(在类似网格的布局中更常见)。

对于任何想快速破解的人来说, 做这个。这将延迟选择,直到它被膨胀和选择。 我不知道如何但它有效。 onFocusSearchFailed 上只有 return null。

 /**
 * Created by sylversphere on 15-04-22.
 */
public class SomeGridLayoutManager extends GridLayoutManager{

    private final Context context;

    public SomeGridLayoutManager(Context context, int spanCount) {
        super(context, spanCount);
        this.context = context;
    }

    public SomeGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
        super(context, spanCount, orientation, reverseLayout);
        this.context = context;
    }

    @Override
    public View onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Recycler recycler, RecyclerView.State state) {
        return null;
    }
}

通过深入挖掘 的答案,我强制回收器通过在其适配器中实现以下内容来回收视图。

@Override
public boolean onFailedToRecycleView(@NonNull VH holder) 
      return true;
}

如你所见here:

Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled due to its transient state. Upon receiving this callback, Adapter can clear the animation(s) that effect the View's transient state and return true so that the View can be recycled. Keep in mind that the View in question is already removed from the RecyclerView.

In some cases, it is acceptable to recycle a View although it has transient state. Most of the time, this is a case where the transient state will be cleared in onBindViewHolder(ViewHolder, int) call when View is rebound to a new position. For this reason, RecyclerView leaves the decision to the Adapter and uses the return value of this method to decide whether the View should be recycled or not.

Note that when all animations are created by RecyclerView.ItemAnimator, you should never receive this callback because RecyclerView keeps those Views as children until their animations are complete. This callback is useful when children of the item views create animations which may not be easy to implement using an RecyclerView.ItemAnimator.

You should never fix this issue by calling holder.itemView.setHasTransientState(false); unless you've previously called holder.itemView.setHasTransientState(true);. Each View.setHasTransientState(true) call must be matched by a View.setHasTransientState(false) call, otherwise, the state of the View may become inconsistent. You should always prefer to end or cancel animations that are triggering the transient state instead of handling it manually.