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

Sri*_*ddy 14 tabs android android-fragments

我有四个标签和四个片段(每个标签各一个).

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

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

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

在此输入图像描述

这是我的适配器代码

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我想指出几件事 -

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

  • 并且提到的其他一些答案使用了相同版本的所有支持库,我已经在所有支持库中使用了25.1.0版本.

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

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

  • 清理构建,invalidate/caches也不起作用.

  • 最后,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();
}
Run Code Online (Sandbox Code Playgroud)

parseLocalDeals method在helper类中,并通过using调用 initializeHotels.initializeRV();

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

编辑2:

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

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

alb*_*elu 0

概括

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

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

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

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

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


1. 部分修复了棉花糖和牛轧糖设备。工作正在进行中。

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

在此输入图像描述

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

在此输入图像描述

我稍后会检查一下,似乎交易已正确加载,但随后加载了趋势并且交易丢失了。此处未完成。

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

在此输入图像描述

在此输入图像描述


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

FragmentStatePagerAdapter 在 Nougat 设备上首次调用 getItem() 时出错

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

FragmentStatePagerAdapter - getItem

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

这些评论在此分支中得到确认并提交

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

在此输入图像描述

在此输入图像描述


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.");
    }
});
}
Run Code Online (Sandbox Code Playgroud)

以前的评论:

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

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

我会将初始化移到 getItem() 之外,如下所示

ViewPager 默认加载下一页(Fragment),您无法通过 setOffscreenPageLimit(0) 更改该页面。但你可以做一些事情来破解。您可以在包含ViewPager的Activity中实现onPageSelected函数。在下一个片段(您不想加载)中,您编写一个函数,比如说 showViewContent() ,您在其中放入所有消耗资源的初始化代码,并且在 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();
    }
}
Run Code Online (Sandbox Code Playgroud)