当前位置: 代码迷 >> Android >> RecyclerView onBindViewHolder 在 Tab 布局中仅调用一次
  详细解决方案

RecyclerView onBindViewHolder 在 Tab 布局中仅调用一次

热度:58   发布时间:2023-08-04 12:19:16.0

我有四个标签和四个片段(每个标签对应一个)。

每个片段都有一个垂直的回收器视图。 由于所有片段视图看起来都相似,因此我重新使用了相同的布局文件、相同的回收器视图项和相同的适配器。

问题是在第一个选项卡和第三个选项卡和第四个选项卡下只加载了一个项目,而第二个选项卡成功加载了整个数据。

我希望下面添加的图片可以更好地理解这个问题。

这是我的适配器代码

public class OthersAdapter extends RecyclerView.Adapter<OthersAdapter.OthersViewHolder> {

    private final Context context;
    private final ArrayList<LocalDealsDataFields> othersDataArray;
    private LayoutInflater layoutInflater;

    public OthersAdapter(Context context, ArrayList<LocalDealsDataFields> othersDataArray) {
        this.context = context;
        this.othersDataArray = othersDataArray;
        if (this.context != null) {
            layoutInflater = LayoutInflater.from(this.context);
        }
    }

    class OthersViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        TextView othersSmallTitleTextView;
        ImageView othersImageView;

        OthersViewHolder(View itemView) {
            super(itemView);
            othersSmallTitleTextView = (TextView) itemView.findViewById(R.id.others_small_title);
            othersImageView = (ImageView) itemView.findViewById(R.id.others_image);
            itemView.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
            Intent couponDetailsItem = new Intent(context, LocalDealsActivity.class);
            Bundle extras = new Bundle();
            extras.putString(Constants.SECTION_NAME, context.getString(R.string.local_deals_section_title));
            // Add the offer id to the extras. This will be used to retrieve the coupon details
            // in the next activity
            extras.putInt(Constants.COUPONS_OFFER_ID, othersDataArray.get(
                    getAdapterPosition()).getLocalDealId());
            couponDetailsItem.putExtras(extras);
            context.startActivity(couponDetailsItem);
        }
    }

    @Override
    public OthersViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = layoutInflater.inflate(R.layout.others_items, parent, false);
        return new OthersViewHolder(view);
    }

    @Override
    public void onBindViewHolder(OthersViewHolder holder, int position) {
        String lfImage = othersDataArray.get(position).getLocalDealImage();
        String lfCategoryName = othersDataArray.get(position).getLocalDealSecondTitle();
        if (lfCategoryName != null) {
            // Set the second title
            holder.othersSmallTitleTextView.setText(lfCategoryName);
        }
        if (lfImage != null) {
            if (!lfImage.isEmpty()) {
                // Get the Uri
                Uri lfUriImage = Uri.parse(lfImage);
                // Load the Image
                Picasso.with(context).load(lfUriImage).into(holder.othersImageView);
            }
        }
    }

    @Override
    public int getItemCount() {
        return othersDataArray.size();
    }
}

我想指出几点——

  • 我已经检查了 Stack Overflow 上的其他答案。 他们谈论将回收器视图layout_height设置为wrap_content 这不是问题,因为layout_height已经是wrap_content并且第二个选项卡也按预期加载了所有数据。

  • 还有一些其他答案提到对所有支持库使用相同的版本,我已经为所有支持库使用了 25.1.0 版本。

  • 数据数组的大小为 20,从适配器的getItemCount()方法返回 20。

  • 数据数组中包含预期数量的项目,并且它们不为 null 或为空。

  • 清理构建,无效/缓存也不起作用。

  • 最后,我正在使用FragmentStatePagerAdapter在选项卡焦点时加载片段。

编辑:

这就是我解析收到的 JSON 数据的方式

private void parseLocalDeals(String stringResponse) throws JSONException {
    JSONArray localJSONArray = new JSONArray(stringResponse);
    // If the array length is less than 10 then display to the end of the JSON data or else
    // display 10 items.
    int localArrayLength = localJSONArray.length() <= 20 ? localJSONArray.length() : 20;
    for (int i = 0; i < localArrayLength; i++) {
        // Initialize Temporary variables
        int localProductId = 0;
        String localSecondTitle = null;
        String localImageUrlString = null;
        JSONObject localJSONObject = localJSONArray.getJSONObject(i);
        if (localJSONObject.has(JSONKeys.KEY_LOCAL_DEAL_ID)) {
            localProductId = localJSONObject.getInt(JSONKeys.KEY_LOCAL_DEAL_ID);
        }
        if (localJSONObject.has(JSONKeys.KEY_LOCAL_DEAL_CATEGORY)) {
            localSecondTitle = localJSONObject.getString(JSONKeys.KEY_LOCAL_DEAL_CATEGORY);
        }
        if (localJSONObject.has(JSONKeys.KEY_LOCAL_DEAL_IMAGE)) {
            localImageUrlString = localJSONObject.getString(JSONKeys.KEY_LOCAL_DEAL_IMAGE);
        }

        if (localImageUrlString != null) {
            if (!localImageUrlString.isEmpty()) {
                // Remove the dots at the start of the Product Image String
                while (localImageUrlString.charAt(0) == '.') {
                    localImageUrlString = localImageUrlString.replaceFirst(".", "");
                }
                // Replace the spaces in the url with %20 (useful if there is any)
                localImageUrlString = localImageUrlString.replaceAll(" ", "%20");
            }
        }

        LocalDealsDataFields localDealsData = new LocalDealsDataFields();
        localDealsData.setLocalDealId(localProductId);
        localDealsData.setLocalDealSecondTitle(localSecondTitle);
        localDealsData.setLocalDealImage(localImageUrlString);

        localDealsDataArray.add(localDealsData);
    }

    // Initialize the Local Deals List only once and notify the adapter that data set has changed
    // from second time. If you initializeRV the localDealsRVAdapter at an early instance and only
    // use the notifyDataSetChanged method here then the adapter doesn't update the data. This is
    // because the adapter won't update items if the number of previously populated items is zero.
    if (localDealsCount == 0) {
        if (localArrayLength != 0) {
            // Populate the Local Deals list
            // Specify an adapter
            localDealsRVAdapter = new OthersAdapter(context, localDealsDataArray);
            localDealsRecyclerView.setAdapter(localDealsRVAdapter);
        } else {
            // localArrayLength is 0; which means there are no rv elements to show.
            // So, remove the layout
            contentMain.setVisibility(View.GONE);
            // Show no results layout
            showNoResultsIfNoData(localArrayLength);
        }
    } else {
        // Notify the adapter that data set has changed
        localDealsRVAdapter.notifyDataSetChanged();
    }
    // Increase the count since parsing the first set of results are returned
    localDealsCount = localDealsCount + 20;
    // Remove the progress bar and show the content
    prcVisibility.success();
}

parseLocalDeals方法位于帮助器类中,通过使用initializeHotels.initializeRV();调用它initializeHotels.initializeRV();

initializeRV()初始化 Recycler 视图,对服务器进行网络调用,并将接收到的数据传递给parseLocalDeals方法。 initializeHotels是 Helper 类的实例变量。

编辑2:

对于那些想要详细探索代码的人,我已将代码的一部分移至另一个项目并在 Github 上共享。 这是链接并了解层次结构,请查看自述文件。

谁能告诉我我错过了什么?

概括

解决了第 1 点的布局问题,将LinearLayout替换为RelativeLayout ,反转可见性逻辑以避免重影效果并捕获异常并在未找到相关视图时阻止它们。

添加第 2 点以证明视觉缺陷仅存在于 Marshmallow 和 Nougat 设备上。

最后FragmentStatePagerAdapter在获得焦点之前加载页面,因此建议在第 3 点进行修复(加载所有页面并在选择时更新它们)。

下面的评论和@d4h中的更多信息。

第四页没有使用相同的布局,只有相同的RecyclerViewid ,也许正在进行中。 布局问题可以使用与前几页相同的布局来解决,但我认为这种更改超出了范围。


1. 部分修复了 Marshmallow 和 Nougat 设备。 工作正在进行中。

Update2 解决了布局问题:

更新:在所有片段初始化中注释initializeTrending也适用于 Api23+

我稍后会检查它,似乎交易已正确加载,但随后加载了趋势并且交易丢失了。 在制品。

,但


2. 您在 Marshmallow 和 Nougat 设备上加载了错误的页面

这最终与 FragmentStatePagerAdapter 代码无关。 相反,在我的片段中,我使用在 init 中传递给片段的字符串(“id”)从数组中获取了一个存储对象。 如果我通过传入数组中对象的位置来获取存储的对象,则没有问题。 仅在搭载 Android 7 的设备中发生。

FragmentStatePager 适配器将加载当前页面,并在任一侧加载一个页面。 这就是为什么它同时记录 0 和 1。 当您切换到第 2 页时,它将加载第 3 页并将第 1 页保留在内存中。 然后当你到达第 4 页时,它不会加载任何东西,因为当你滚动到 3 时加载了 4,除此之外没有任何东西。 因此,您在 getItem() 中获得的 int 不是当前正在查看的页面,而是正在加载到内存中的页面。 希望能帮您解决问题

这些评论中得到确认

所有页面都在 Lollipop 模拟器上正确加载,最后一页有一个额外的问题,请参阅OthersFragment


3. 在创建时初始化所有页面并在选择时更新它们。

这些更改解决了下面评论的问题:

/**
 * Implement the tab layout and view pager
 */
private void useSlidingTabViewPager() {
    // Create the adapter that will return a fragment for each of the three
    // primary sections of the activity.
    BottomSectionsPagerAdapter mBottomSectionsPagerAdapter = new BottomSectionsPagerAdapter(getChildFragmentManager());

    // Set up the ViewPager with the sections adapter.
    ViewPager mBottomViewPager = (ViewPager) rootView.findViewById(R.id.local_bottom_pager);
    mBottomViewPager.setOffscreenPageLimit(mBottomSectionsPagerAdapter.getCount());
    mBottomViewPager.setAdapter(mBottomSectionsPagerAdapter);

    TabLayout tabLayout = (TabLayout) rootView.findViewById(R.id.tab_layout);
    tabLayout.setupWithViewPager(mBottomViewPager);
    tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {

    /**
     * Called when a tab enters the selected state.
     *
     * @param tab The tab that was selected
     */
    @Override
    public void onTabSelected(TabLayout.Tab tab) {
        // TODO: update the selected page here
        Log.i(LOG_TAG, "page " + tab.getPosition() + " selected.");
    }

    /**
     * Called when a tab exits the selected state.
     *
     * @param tab The tab that was unselected
     */
    @Override
    public void onTabUnselected(TabLayout.Tab tab) {
        // Do nothing
        Log.i(LOG_TAG, "Page " + tab.getPosition() + " unselected and ");
    }

    /**
     * Called when a tab that is already selected is chosen again by the user. Some applications
     * may use this action to return to the top level of a category.
     *
     * @param tab The tab that was reselected.
     */
    @Override
    public void onTabReselected(TabLayout.Tab tab) {
        // Do nothing
        Log.i(LOG_TAG, "Page " + tab.getPosition() + " reselected.");
    }
});
}

以前的评论:

使用断点检查您的 LocalFragment getItem() 方法。

如果你选择一个页面,下一个页面也会被初始化,并且你正在共享recyclerView等。

我会按照建议将初始化移到 getItem() 之外:

ViewPager 默认加载下一页(Fragment),您无法通过 setOffscreenPageLimit(0) 更改该页面。 但是你可以做一些事情来破解。 您可以在包含 ViewPager 的 Activity 中实现 onPageSelected 函数。 在下一个 Fragment(您不想加载)中,您编写了一个函数,比如 showViewContent(),您将所有资源消耗的初始化代码放入其中,并且在 onResume() 方法之前不执行任何操作。 然后在onPageSelected 中调用showViewContent() 函数。 希望这会有所帮助

阅读这些相关问题(第一个有可能的解决方法将限制破解为零):

ViewPager 是否至少需要 1 个离屏页面?

是的。 如果我正确阅读源代码,您应该会在 LogCat 中收到关于此的警告,例如:

请求的屏幕外页面限制 0 太小; 默认为 1

viewPager.setOffscreenPageLimit(couponsPagerAdapter.getCount());

public void setOffscreenPageLimit(int limit) {
    if (limit < DEFAULT_OFFSCREEN_PAGES) {
        Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                + DEFAULT_OFFSCREEN_PAGES);
        limit = DEFAULT_OFFSCREEN_PAGES;
    }
    if (limit != mOffscreenPageLimit) {
        mOffscreenPageLimit = limit;
        populate();
    }
}

答案不多,但评论太长了。

我复制了(几乎)你的适配器代码,它完全适合我。 我相信我和你做了同样的事情。 我为所有选项卡使用相同的布局文件、相同的项目和相同的适配器。 我认为您的适配器代码没有问题。

我说“几乎”是因为我无法访问您的数据,因此我不得不更改一些内容。 我更改了您的LocalDealsDataField模型以包含一个 BitmapDrawable 并且我更改了onBindViewHolder()来处理它。

    BitmapDrawable lfImage = othersDataArray.get(position).getLocalDealImage();
    holder.othersImageView.setBackground(lfImage);

由于您的适配器似乎没有问题,因此我将专注于获取数据或将适配器设置为您的问题。 抱歉,除此之外我无能为力。

仅供参考,这是我在onCreateView()设置适配器的方法

    rootView = inflater.inflate(R.layout.recycler_view, container, false);
    mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview);
    mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
    mAdapter = new OthersAdapter(this.getContext(), list);
    mRecyclerView.setAdapter(mAdapter);

我看过你的代码,问题和@ardock 解释的一样

我想提出的解决方案,

您必须在 3 个地方更改代码 ::

  1. 在您在ViewPager中使用的所有Fragment内不要从onCreateView方法调用initializeRESPECTIVEView()

  2. LocalFragment列出您将与ViewPager一起使用的ViewPager并将其传递给BottomSectionsPagerAdapter 并返回Fragment从该列表由getItem(int position)BottomSectionsPagerAdapter

  3. 下面的代码添加LocalFragmentuseSlidingTabViewPager()

tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {   

    @Override
    public void onTabSelected(TabLayout.Tab tab) {

    }

    @Override
    public void onTabUnselected(TabLayout.Tab tab) {

    }

    @Override
    public void onTabReselected(TabLayout.Tab tab) {

    }
});

//从onTabSelected调用各自的片段initializeRESPECTIVEView()方法,您可以从传递给BottomSectionsPagerAdapter列表中获取片段实例

  相关解决方案