22

I'm developing on Amazon Fire TV.

Because it's a TV app(No touch), I need focusables inside row's layout to be able to navigate around.

I have a really simple Recyclerview with image, text, and a focusable. When I press up or down, it all scrolls and stuff correctly, but I noticed that when I navigate faster than scroll can keep up, it creates new viewholders (Off screen) and lags up the UI.

I have created an activity with Creation numbers on it. When I scroll slowly, the highest creation # is 10. But when I scroll fast, I get cards with creation number 60 in a second. This causes an enormous lag and the application drops a lot of frames. Is my approach totally wrong?

Use the code below to test this out.

/**
 * 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 (Urls not owned by me)

<?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 is #9000

glass_focus is #0000

Vasily Kabunov
  • 6,511
  • 13
  • 49
  • 53
Dreamingwhale
  • 433
  • 6
  • 22
  • 1
    Probably because the image loading is taking a long time in your `onBindViewHolder()`. I haven't worked with Picasso so not sure if it's possible but I what I've done is to asynchronously load the images with a delay in my recycler (I use Universal Image Loader and in that, it's definitely possible). This fixed problems with smooth scrolling and in turn, creating too many views. Just a thought which may or may not help you but worth a try. – kha Apr 21 '15 at 15:36
  • I am not sure if this is the true now, but when android 5 was still in developer preview, RecyclerView was really dumb. If you jumped from postion 5 to 5k it called onBind for all views between those numbers. That caused an enormous delay – Bojan Kseneman Apr 21 '15 at 15:43
  • @kha I have tried this method as well. It did work a bit but I had to do at least 700 millis delay which made even cached images to load slow. Then hack ensued. :( – Dreamingwhale Apr 21 '15 at 16:02
  • @BojanKseneman Does this mean I"m using the wrong version of Recyclerview? I do compile with V22. So I'm not sure if this is the issue. – Dreamingwhale Apr 21 '15 at 16:03
  • As I said I am not sure if that is the case any longer. I'm pretty sure google fixed that by now. – Bojan Kseneman Apr 21 '15 at 16:04
  • @BojanKseneman Sorry sir, true true. Thank you :) – Dreamingwhale Apr 21 '15 at 16:08
  • 1
    @BojanKseneman not sure where you got that position 5 to 5k information but that is completely wrong information, it was never ever the case. @Dreamingwhale, you might be losing view holders due to transient state, can you override `onFailedToRecycleView` in the Adapter and see if you are receiving calls to it ? – yigit Apr 21 '15 at 16:20
  • 1
    I just tried this and it is working as it should. Also I was scrolling in code :) – Bojan Kseneman Apr 21 '15 at 16:25
  • @Override public boolean onFailedToRecycleView(SickViewHolder holder) { System.err.println("Failed to recycle"); return super.onFailedToRecycleView(holder); } This never calls once. – Dreamingwhale Apr 21 '15 at 17:01
  • I feel like it's more the focus going too fast and that recyclerview knows that there might be a focusable in the next row and it creates new ones so that I can focus on it. So it creates a lot of offscreen views when I'm holding DPAD_DOWN. – Dreamingwhale Apr 21 '15 at 17:02
  • @BojanKseneman This code works on mobile nomatter how fast I scroll. When I use my finger to scroll, recycle works properly. It's the focusing that ,I think, causes this explosive offscreen viewholder creation. – Dreamingwhale Apr 21 '15 at 17:03
  • @Dreamingwhale And what will happen, if you remove the image loading in onBindviewHolder()? – traninho Apr 25 '15 at 12:34
  • @traninho It will still lag. A bit less but the pattern of lag is the same. – Dreamingwhale Apr 27 '15 at 14:21
  • If I hold the down button, framerate will drop increasingly until the navigation has hit the bottom of the list, then frame rate increases and list scrolls all the way to the bottom smoothly. – Dreamingwhale Apr 27 '15 at 14:22

5 Answers5

20

Try increasing the maximum number of recycled views in the pool:

recyclerView.getRecycledViewPool().setMaxRecycledViews(50);

50 is an arbitrary number, you can try higher or lower and see what happens.

RecyclerView tries to avoid re-using views that have transient state, so if views are invalidated quickly or are animating, they may not be re-used right away.

Similarly if you have many smaller views, you may end up with more on screen than the default pool size can handle (more common with grid like layouts).

DiscDev
  • 38,652
  • 20
  • 117
  • 133
Sam Judd
  • 7,317
  • 1
  • 38
  • 38
  • When I tried this with numbers 5, 10, 15, 30, 50, 100, 150, 1000, Immediate changes were not noticed. I'll do some research on transient state and get back to this. Thank you so much for the explanation. I'm seeing some light. :) – Dreamingwhale Apr 28 '15 at 21:45
  • 1
    This helped me when I was implementing a fast scroller. It uses `scrollToPosition`, and without the pool size adjustment `RecyclerView` makes me inflate the layout for all the items each time a new position is set. Needless to say, this is no good for performance. – Malcolm Nov 17 '15 at 21:57
  • Thanks a lot! This has solved a performance issue on the fast scrolling!! – Kirill Vashilo Nov 30 '16 at 18:40
  • it's useful for me,thanx! – peng gao Dec 18 '19 at 12:37
3

Picasso is holding you up but the suggested solution to build your own mechanism is not the way to go.

Picasso has fallen behind in the last year or so and today there are far better alternatives in the form of Google Glide and Facebook Fresco which specifically released updates to work better with RecyclerView and have proved faster and more efficient in loading, caching and storing in many tests which are available online such as:

I hope that helped. Good luck.

Royi Benyossef
  • 769
  • 5
  • 18
  • Yes image library and all makes sense. But this happens even without the images. I've tried UIL, Picasso, Glide and I am currently using Glide. A more fundamental problem is my recyclerview creating new viewholders for the ones that are off screen. This happens because I navigate with focusables. The time it takes to inflate is the real killer here. – Dreamingwhale Apr 27 '15 at 14:20
  • Thank you. I know Glide but I didn't get any incentive to use it in place of Picasso. Now I am sold since I am moving forward intensively with RecyclerView. – Ralphilius Aug 22 '15 at 07:13
  • Proved to be faster where? – Daniel Gomez Rico Nov 16 '16 at 03:18
2

As the commenters pointed out pending responses from Picasso might be holding you up. If that is the case, you can solve it by extending ImageView and overriding the following method. I think it is worth trying.

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

Update:

Turns out this is not the correct way, anyone wanting to cancel requests should do so in the onViewRecycled() callback as pointed out in the comments below.

Oguz Babaoglu
  • 299
  • 2
  • 7
  • Thank you, I had high hopes for this one but it doesn't seem to work. I tried 1000 elements and scroll through them by holding down key, ended up creating 330 ~ 370 whether or not I use this piece of code. – Dreamingwhale Apr 21 '15 at 16:15
  • 1
    This is wrong, a View may be detached and cached by RecyclerView. Instead, you can call Picasso cancel when RecyclerView calls Adapter's onVIewRecycled callback. – yigit Apr 21 '15 at 16:23
  • @yigit Thank you. But I don't think the Image loading is the problem. I can take out the image loading call and it'll still have created 200 new cards (Even though about 10 is all that's needed). – Dreamingwhale Apr 21 '15 at 16:58
  • yea i don't think it is the problem either, i just wanted to clarify that you cannot cancel load on detach. – yigit Apr 21 '15 at 19:51
1

By digging deeper in Sam Judd's answer I forced recycler to recycle the views by implementing the followings in its adapter.

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

As you can see 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.

hadilq
  • 1,023
  • 11
  • 25
-2

For anyone else looking for a quick hack, Do this. This will delay selection until it's been inflated and selected. I don't know how but it works. Just return null on onFocusSearchFailed.

 /**
 * 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;
    }
}
Dreamingwhale
  • 433
  • 6
  • 22