RecyclerView nhấp nháy sau khitifyDatasetChanged ()


113

Tôi có RecyclerView tải một số dữ liệu từ API, bao gồm url hình ảnh và một số dữ liệu và tôi sử dụng networkImageView để tải hình ảnh lười biếng.

@Override
public void onResponse(List<Item> response) {
   mItems.clear();
   for (Item item : response) {
      mItems.add(item);
   }
   mAdapter.notifyDataSetChanged();
   mSwipeRefreshLayout.setRefreshing(false);
}

Đây là cách triển khai cho Bộ điều hợp:

public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, final int position) {
        if (isHeader(position)) {
            return;
        }
        // - get element from your dataset at this position
        // - replace the contents of the view with that element
        MyViewHolder holder = (MyViewHolder) viewHolder;
        final Item item = mItems.get(position - 1); // Subtract 1 for header
        holder.title.setText(item.getTitle());
        holder.image.setImageUrl(item.getImg_url(), VolleyClient.getInstance(mCtx).getImageLoader());
        holder.image.setErrorImageResId(android.R.drawable.ic_dialog_alert);
        holder.origin.setText(item.getOrigin());
    }

Vấn đề là khi chúng tôi làm mới trong Chế độ xem tái chế, nó nhấp nháy trong một thời gian rất ngắn trong lúc ban đầu trông rất lạ.

Tôi chỉ sử dụng GridView / ListView thay thế và nó hoạt động như tôi mong đợi. Không có chớp mắt.

cấu hình cho RecycleView trong onViewCreated of my Fragment:

mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
        // use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView
        mRecyclerView.setHasFixedSize(true);

        mGridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
        mGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                return mAdapter.isHeader(position) ? mGridLayoutManager.getSpanCount() : 1;
            }
        });

        mRecyclerView.setAdapter(mAdapter);

Bất cứ ai phải đối mặt với một vấn đề như vậy? Điều gì có thể là lý do?


Câu trả lời:


126

Hãy thử sử dụng các ID ổn định trong RecyclerView.Adapter của bạn

setHasStableIds(true)và ghi đè getItemId(int position).

Nếu không có ID ổn định notifyDataSetChanged(), các ViewHolders thường được gán cho các vị trí không giống nhau. Đó là lý do nhấp nháy trong trường hợp của tôi.

Bạn có thể tìm thấy một lời giải thích tốt ở đây.


1
Tôi đã thêm setHasStableIds (true), giải quyết hiện tượng nhấp nháy, nhưng trong GridLayout của tôi, các mục vẫn thay đổi vị trí khi cuộn. Tôi nên ghi đè những gì trong getItemId ()? Cảm ơn
Balázs Orbán

3
Bất kỳ ý tưởng nào về cách chúng tôi nên tạo ID?
Mauker

Bạn thật tuyệt vời! Google KHÔNG. Google hoặc không biết về điều này HOẶC họ biết và không muốn chúng tôi biết! CẢM ƠN
MBH

1
lộn xộn các vị trí các mục, các mục đến từ cơ sở dữ liệu firebase theo đúng thứ tự.
MuhammadAliJr

1
@Mauker Nếu đối tượng của bạn có một số duy nhất, bạn có thể sử dụng số đó hoặc nếu bạn có một chuỗi duy nhất, bạn có thể sử dụng object.hashCode (). Đó là công trình hoàn toàn tốt đẹp đối với tôi
Simon Schubert

106

Theo đó vấn đề trang .... đó là những hình ảnh động mặc định thay đổi mục recycleview ... Bạn có thể tắt nó đi .. cố gắng này

recyclerView.getItemAnimator().setSupportsChangeAnimations(false);

Thay đổi trong phiên bản mới nhất

Trích dẫn từ blog của nhà phát triển Android :

Lưu ý rằng API mới này không tương thích ngược. Nếu trước đây bạn đã triển khai ItemAnimator, bạn có thể mở rộng SimpleItemAnimator để thay thế, cung cấp API cũ bằng cách gói API mới. Bạn cũng sẽ nhận thấy rằng một số phương thức đã bị xóa hoàn toàn khỏi ItemAnimator. Ví dụ: nếu bạn đang gọi RecyclerView.getItemAnimator (). SetSupportsChangeAnimations (false), mã này sẽ không biên dịch nữa. Bạn có thể thay thế nó bằng:

ItemAnimator animator = recyclerView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
  ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}

5
@sabeer vẫn còn vấn đề. Nó không giải quyết vấn đề.
Shreyash Mahajan

3
@delive cho tôi biết nếu bạn tìm thấy bất kỳ giải pháp này
Shreyash Mahajan

Tôi sử dụng picasso, một cái gì đó của nó, không xuất hiện nhấp nháy.
delive

8
Kotlin: (recyclerView.itemAnimator như SimpleItemAnimator?) ?. supportsChangeAnimations = false
Pedro Paulo Amorim

Nó đang hoạt động, nhưng sự cố khác sắp xảy ra (NPE) java.lang.NullPointerException: Cố gắng đọc từ trường 'int android.support.v7.widget.RecyclerView $ ItemAnimator $ ItemHolderInfo.left' trên tham chiếu đối tượng rỗng Có cách nào không để giải quyết điều này?
Navas pk

46

Điều này chỉ đơn giản là hoạt động:

recyclerView.getItemAnimator().setChangeDuration(0);

đó là một lựa chọn thay thế tốt cho codeItemAnimator animator = reclerView.getItemAnimator (); if (animator instanceof SimpleItemAnimator) {((SimpleItemAnimator) animator) .setSupportsChangeAnimations (false); }code
ziniestro

1
dừng tất cả các hoạt ảnh THÊM và XÓA
MBH

11

Tôi gặp vấn đề tương tự khi tải hình ảnh từ một số url và sau đó imageView nhấp nháy. Giải quyết bằng cách sử dụng

notifyItemRangeInserted()    

thay vì

notifyDataSetChanged()

tránh tải lại các dữ liệu cũ không thay đổi đó.


9

thử điều này để tắt hoạt ảnh mặc định

ItemAnimator animator = recyclerView.getItemAnimator();

if (animator instanceof SimpleItemAnimator) {
  ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}

Đây là cách mới để tắt hoạt ảnh kể từ khi hỗ trợ Android 23

cách cũ này sẽ hoạt động cho phiên bản cũ hơn của thư viện hỗ trợ

recyclerView.getItemAnimator().setSupportsChangeAnimations(false)

4

Giả sử mItemslà bộ sưu tập ủng hộ bạn Adapter, tại sao bạn lại xóa mọi thứ và thêm lại? Về cơ bản bạn đang nói với nó rằng mọi thứ đã thay đổi, vì vậy RecyclerView liên kết tất cả các chế độ xem hơn là tôi cho rằng thư viện Hình ảnh không xử lý nó đúng cách, nơi nó vẫn đặt lại Chế độ xem mặc dù đó là cùng một url hình ảnh. Có thể họ đã có một số giải pháp cho AdapterView để nó hoạt động tốt trong GridView.

Thay vì gọi notifyDataSetChangedmà sẽ gây ra ràng buộc lại tất cả các chế độ xem, hãy gọi các sự kiện thông báo chi tiết (thông báo đã thêm / xóa / di chuyển / cập nhật) để RecyclerView sẽ chỉ liên kết lại các chế độ xem cần thiết và không có gì nhấp nháy.


3
Hoặc có thể nó chỉ là một "lỗi" trong RecyclerView? Rõ ràng là nếu nó hoạt động tốt trong 6 năm qua với AbsListView và bây giờ nó không hoạt động với RecyclerView, có nghĩa là có điều gì đó không ổn với RecyclerView, phải không? :) Nhìn nhanh vào nó cho thấy rằng khi bạn làm mới dữ liệu trong ListView và GridView, chúng sẽ theo dõi chế độ xem + vị trí, vì vậy khi bạn làm mới, bạn sẽ nhận được chính xác cùng một trình xem. Trong khi RecyclerView xáo trộn các chủ sở hữu chế độ xem, dẫn đến hiện tượng nhấp nháy.
vovkab

Làm việc trên ListView không có nghĩa là nó đúng với RecyclerView. Các thành phần này có kiến ​​trúc khác nhau.
yigit

1
Đồng ý, nhưng đôi khi rất khó để biết những mục nào được thay đổi, chẳng hạn như nếu bạn sử dụng con trỏ hoặc bạn chỉ làm mới toàn bộ dữ liệu của mình. Vì vậy, Recyclerview cũng nên xử lý trường hợp này một cách chính xác.
vovkab

4

Recyclerview sử dụng DefaultItemAnimator làm trình tạo hoạt ảnh mặc định. Như bạn có thể thấy từ mã bên dưới, họ thay đổi alpha của người giữ chế độ xem khi thay đổi mặt hàng:

@Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) {
    ...
    final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
    ...
    ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
    if (newHolder != null) {
        ....
        ViewCompat.setAlpha(newHolder.itemView, 0);
    }
    ...
    return true;
}

Tôi muốn giữ lại phần còn lại của các hoạt ảnh nhưng loại bỏ "nhấp nháy" vì vậy tôi đã nhân bản DefaultItemAnimator và loại bỏ 3 dòng alpha ở trên.

Để sử dụng trình tạo hoạt ảnh mới, chỉ cần gọi setItemAnimator () trên RecyclerView của bạn:

mRecyclerView.setItemAnimator(new MyItemAnimator());

Nó đã loại bỏ nhấp nháy nhưng nó gây ra hiệu ứng nhấp nháy vì một số lý do.
Mohamed Medhat

4

Trong Kotlin, bạn có thể sử dụng 'phần mở rộng lớp' cho RecyclerView:

fun RecyclerView.disableItemAnimator() {
    (itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
}

// sample of using in Activity:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    // ...
    myRecyclerView.disableItemAnimator()
    // ...
}

1

Này @Ali nó có thể phát lại muộn. Tôi cũng đã đối mặt với vấn đề này và đã giải quyết bằng giải pháp dưới đây, nó có thể giúp bạn vui lòng kiểm tra.

Lớp LruBitmapCache.java được tạo để lấy kích thước bộ nhớ cache hình ảnh

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
import com.android.volley.toolbox.ImageLoader.ImageCache;

public class LruBitmapCache extends LruCache<String, Bitmap> implements
        ImageCache {
    public static int getDefaultLruCacheSize() {
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        final int cacheSize = maxMemory / 8;

        return cacheSize;
    }

    public LruBitmapCache() {
        this(getDefaultLruCacheSize());
    }

    public LruBitmapCache(int sizeInKiloBytes) {
        super(sizeInKiloBytes);
    }

    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight() / 1024;
    }

    @Override
    public Bitmap getBitmap(String url) {
        return get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        put(url, bitmap);
    }
}

VolleyClient.java singleton [mở rộng Ứng dụng] được thêm vào mã bên dưới

trong phương thức tạo lớp singleton VolleyClient, hãy thêm đoạn mã bên dưới để khởi tạo ImageLoader

private VolleyClient(Context context)
    {
     mCtx = context;
     mRequestQueue = getRequestQueue();
     mImageLoader = new ImageLoader(mRequestQueue,getLruBitmapCache());
}

Tôi đã tạo phương thức getLruBitmapCache () để trả về LruBitmapCache

public LruBitmapCache getLruBitmapCache() {
        if (mLruBitmapCache == null)
            mLruBitmapCache = new LruBitmapCache();
        return this.mLruBitmapCache;
}

Hy vọng nó sẽ giúp bạn.


Cảm ơn câu trả lời của bạn. Đó chính xác là những gì tôi đã làm trong VollyClient.java của mình. Hãy xem: VolleyClient.java
Ali

Chỉ cần kiểm tra một lần bằng cách sử dụng lớp LruCache <String, Bitmap>, tôi nghĩ nó sẽ giải quyết được vấn đề của bạn. Hãy xem LruCache
học Android

Bạn đã kiểm tra một lần mã mà tôi đã chia sẻ với bạn trong bình luận chưa? bạn có gì trong lớp của bạn mà tôi đã bỏ lỡ ở đó?
Ali

Bạn đang thiếu mở rộng lớp LruCache <String, Bitmap> và ghi đè phương thức sizeOf () như cách tôi đã làm, còn lại tất cả đều có vẻ ổn đối với tôi.
học Android

Được rồi, tôi sẽ thử rất sớm, nhưng bạn có thể giải thích cho tôi biết bạn đã làm gì ở đó để mang lại điều kỳ diệu cho bạn và giải quyết vấn đề không? sourcecode Đối với tôi, có vẻ như phương thức sizeOf bị ghi đè của bạn phải nằm trong mã nguồn.
Ali

1

đối với tôi recyclerView.setHasFixedSize(true);đã làm việc


Tôi không nghĩ các mặt hàng của OP có kích thước cố định
Irfandi D. Vendy

điều này đã giúp tôi trong trường hợp của tôi
bst91

1

Trong trường hợp của tôi, không có bất kỳ câu trả lời nào ở trên hoặc câu trả lời từ các câu hỏi stackoverflow khác có vấn đề tương tự hoạt động.

Chà, tôi đang sử dụng hoạt ảnh tùy chỉnh mỗi khi mục được nhấp vào, mà tôi đang gọi thông báo với nó làtifyItemChanged (int position, Object Payload) để chuyển tải trọng cho lớp CustomAnimator của tôi.

Lưu ý, có 2 phương thức onBindViewHolder (...) có sẵn trong RecyclerView Adapter. Phương thức onBindViewHolder (...) có 3 tham số sẽ luôn được gọi trước phương thức onBindViewHolder (...) có 2 tham số.

Nói chung, chúng tôi luôn ghi đè phương thức onBindViewHolder (...) có 2 tham số và gốc rễ chính của vấn đề là tôi đã làm như vậy, vì mỗi lần thông báo ItemChanged (...) được gọi, phương thức onBindViewHolder (...) của chúng tôi sẽ được gọi, trong đó tôi đang tải hình ảnh của mình trong ImageView bằng Picasso, và đây là lý do khiến nó tải lại bất kể từ bộ nhớ hay từ internet. Cho đến khi tải, nó đang hiển thị cho tôi hình ảnh trình giữ chỗ, đó là lý do nhấp nháy trong 1 giây bất cứ khi nào tôi nhấp vào chế độ xem mục.

Sau đó, tôi cũng ghi đè một phương thức onBindViewHolder (...) khác có 3 tham số. Ở đây tôi kiểm tra xem danh sách các trọng tải có trống không, sau đó tôi trả về việc triển khai siêu lớp của phương thức này, nếu không có các trọng tải, tôi chỉ đang đặt giá trị alpha của itemView của chủ sở hữu thành 1.

Và đúng là tôi đã có giải pháp cho vấn đề của mình sau khi lãng phí một ngày buồn bã!

Đây là mã của tôi cho các phương thức onBindViewHolder (...):

onBindViewHolder (...) với 2 tham số:

@Override
public void onBindViewHolder(@NonNull RecyclerAdapter.ViewHolder viewHolder, int position) {
            Movie movie = movies.get(position);

            Picasso.with(context)
                    .load(movie.getImageLink())
                    .into(viewHolder.itemView.posterImageView);
    }

onBindViewHolder (...) với 3 tham số:

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
        if (payloads.isEmpty()) {
            super.onBindViewHolder(holder, position, payloads);
        } else {
            holder.itemView.setAlpha(1);
        }
    }

Đây là mã của phương thức tôi đã gọi trong onClickListener của itemView của viewHolder trong onCreateViewHolder (...):

private void onMovieClick(int position, Movie movie) {
        Bundle data = new Bundle();
        data.putParcelable("movie", movie);

        // This data(bundle) will be passed as payload for ItemHolderInfo in our animator class
        notifyItemChanged(position, data);
    }

Lưu ý: Bạn có thể có được vị trí này bằng cách gọi phương thức getAdapterPosition () của viewHolder của bạn từ onCreateViewHolder (...).

Tôi cũng đã ghi đè phương thức getItemId (int position) như sau:

@Override
public long getItemId(int position) {
    Movie movie = movies.get(position);
    return movie.getId();
}

và được gọi setHasStableIds(true); đối tượng bộ điều hợp của tôi đang hoạt động.

Hy vọng điều này sẽ hữu ích nếu không có câu trả lời nào ở trên hoạt động!


1

Trong trường hợp của tôi, có một vấn đề đơn giản hơn nhiều, nhưng nó có thể trông giống như vấn đề ở trên. Tôi đã chuyển đổi một ExpandableListView thành RecylerView với Groupie (sử dụng tính năng ExpandableGroup của Groupie). Bố cục ban đầu của tôi có một phần như thế này:

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/hint_list"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:background="@android:color/white" />

Với layout_height được đặt thành "wrap_content", hoạt ảnh từ nhóm mở rộng sang nhóm thu gọn có cảm giác như nó sẽ nhấp nháy, nhưng nó thực sự chỉ hoạt động từ vị trí "sai" (ngay cả sau khi thử hầu hết các đề xuất trong chuỗi này).

Dù sao, chỉ cần thay đổi layout_height thành match_parent như thế này đã khắc phục được sự cố.

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/hint_list"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="@android:color/white" />

0

Tôi đã gặp sự cố tương tự và cách này phù hợp với tôi. Bạn có thể gọi phương pháp này để đặt kích thước cho bộ nhớ cache hình ảnh

private int getCacheSize(Context context) {

    final DisplayMetrics displayMetrics = context.getResources().
            getDisplayMetrics();
    final int screenWidth = displayMetrics.widthPixels;
    final int screenHeight = displayMetrics.heightPixels;
    // 4 bytes per pixel
    final int screenBytes = screenWidth * screenHeight * 4;

    return screenBytes * 3;
}

0

đối với ứng dụng của mình, tôi đã thay đổi một số dữ liệu nhưng tôi không muốn toàn bộ chế độ xem nhấp nháy.

Tôi đã giải quyết nó bằng cách chỉ làm mờ oldview xuống 0,5 alpha và bắt đầu newview alpha ở 0,5. Điều này tạo ra một chuyển đổi mờ dần nhẹ nhàng hơn mà không làm cho chế độ xem biến mất hoàn toàn.

Thật không may vì triển khai riêng tư, tôi không thể phân lớp DefaultItemAnimator để thực hiện thay đổi này, vì vậy tôi phải sao chép mã và thực hiện các thay đổi sau

trong animateChange:

ViewCompat.setAlpha(newHolder.itemView, 0);  //change 0 to 0.5f

trong animateChangeImpl:

oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { //change 0 to 0.5f

0

Sử dụng các phương pháp tái chế thích hợp để cập nhật chế độ xem sẽ giải quyết được vấn đề này

Đầu tiên, thực hiện các thay đổi trong danh sách

mList.add(item);
or mList.addAll(itemList);
or mList.remove(index);

Sau đó thông báo bằng

notifyItemInserted(addedItemIndex);
or
notifyItemRemoved(removedItemIndex);
or
notifyItemRangeChanged(fromIndex, newUpdatedItemCount);

Hy vọng điều này sẽ giúp !!


Tuyệt đối không. Nếu bạn đang thực hiện một số cập nhật theo giây (trong trường hợp của tôi là quét BLE) thì nó hoàn toàn không hoạt động. Tôi đã dành một ngày để cập nhật lên RecyclerAdapter chết tiệt này ... Tốt hơn là nên giữ ArrayAdapter. Thật tiếc khi không sử dụng mẫu MVC, nhưng ít nhất nó gần như có thể sử dụng được.
Gojir4


0

Giải pháp Kotlin:

(recyclerViewIdFromXML.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.