RecyclerView在Tab布局中,onBindViewHolder方法只被调用一次

19
我有四个选项卡和四个片段(每个选项卡对应一个片段)。
每个片段都有一个垂直的可回收视图。由于所有片段的视图看起来相似,因此我重复使用相同的布局文件、相同的可回收视图项和相同的适配器。
问题是:只有第一个选项卡和第三个选项卡以及第四个选项卡加载了一个项目,而第二个选项卡成功加载了整个数据。
我希望下面添加的图片能更好地说明这个问题。

enter image description here

这是我的适配器代码

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_heightwrap_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(); 调用。

initializeRV() 初始化 Recycler 视图,向服务器发起网络调用,接收到的数据传递给 parseLocalDeals 方法。其中 initializeHotels 是 Helper 类的实例变量。

编辑 2:

对于那些想要深入了解代码的人,我已将代码部分移动到另一个项目并在 Github 上分享。这是链接 https://github.com/gSrikar/TabLayout,要了解层次结构,请查看 README 文件。

有人能告诉我我错过了什么吗?


评论不适合进行长时间的讨论;此对话已被移至聊天室 - Bhargav Rao
3个回答

0

我已经查看了您的代码,问题与@ardock所解释的相同。

我想提出的解决方案是,

您需要在3个位置更改您的代码:

  1. 在所有您在ViewPager中使用的Fragment内部,请勿从onCreateView方法调用initializeRESPECTIVEView()

  2. LocalFragment内部,制作一个您要与ViewPager一起使用的Fragment列表,并将其传递给BottomSectionsPagerAdapter。 从BottomSectionsPagerAdaptergetItem(int position)返回该列表中的Fragment

  3. useSlidingTabViewPager()内部的LocalFragment中添加以下代码。

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的列表中获取片段实例


0

摘要

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

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

最后,因为 FragmentStatePagerAdapter 在获取焦点之前加载页面,所以在 点3 提出了一个修复方案(加载所有页面并在选择时更新它们)。

更多信息请参见下面的评论和 @d4h answer

第四页没有使用相同的布局,只有相同的 RecyclerViewid,可能是一项正在进行中的工作。可以使用与之前页面相同的布局来解决布局问题,但我认为这种更改超出了范围。


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

更新2通过将LinearLayout更改为RelativeLayout并反转可见性逻辑解决了布局问题:

enter image description here

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

enter image description here

我稍后会检查一下,看起来交易已经正确加载,但是趋势加载后交易就丢失了。WIP 这里

如果趋势数组为空且趋势视图消失,则不会显示交易, 但是使用不可见的方式显示

enter image description here

enter image description here


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

在Nougat设备上,FragmentStatePagerAdapter第一次调用getItem()时出现错误

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

FragmentStatePagerAdapter - getItem

一个FragmentStatePager适配器将加载当前页面和相邻的一页。这就是为什么它同时记录0和1的原因。当您切换到第2页时,它将加载第3页并将第1页保留在内存中。然后,当您到达第4页时,它将不会加载任何内容,因为在滚动到第3页时已经加载了第4页,并且没有超出那个范围的内容。因此,在getItem()中给出的int值不是当前正在查看的页面,而是正在加载到内存中的页面。希望这能为您澄清问题。
这些评论已在此分支和提交中得到确认。
所有页面在Lollipop模拟器上正确加载,最后一页有一个额外的问题,请参见OthersFragment

enter image description here

enter image description here


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

增加OffScreenPageLimit以初始化所有页面

添加页面选中/取消选中/重新选中监听器

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

/**
 * 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(您不想加载的Fragment)中,您编写一个名为showViewContent()的函数,在其中放置所有资源消耗的init代码,并在onResume()方法之前什么都不做。然后在onPageSelected中调用showViewContent()函数。希望这能帮助到您。

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

ViewPager.setOffscreenPageLimit(0)不能按预期工作

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();
    }
}

0

虽然不是很好的答案,但是太长了不能作为评论。

我已经复制了(几乎)你的适配器代码,并且它完全可以正常工作。我相信我做的和你一样。我使用相同的布局文件、相同的项目和相同的适配器来处理所有选项卡。我认为你的适配器代码没有问题。

我说“几乎”是因为我必须改变一些东西,因为我无法访问你的数据。我将你的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);

我已将代码添加到Github,您可以在问题中找到链接。此外,这是示例JSON数据https://github.com/gSrikar/TabLayout/tree/master/samplejson - Srikar Reddy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接