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

时间:2017-02-16 17:25:27

标签: android android-fragments tabs

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

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

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

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

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_height设置为wrap_content。这不是问题,因为layout_height已经wrap_content,第二个标签也按预期加载了所有数据。

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

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

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

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

  • 最后,当标签处于焦点时,我使用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,要了解层次结构,请查看自述文件。

谁能告诉我我失踪了什么?

3 个答案:

答案 0 :(得分:0)

摘要

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

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

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

以下评论中的更多信息和@ d4h answer

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

1。部分固定用于棉花糖和牛轧糖设备。正在进行中。

Update2 Changing LinearLayout by RelativeLayout and inverting visibility logic解决了布局问题:

enter image description here

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

enter image description here

我稍后会检查,似乎交易已正确加载,但随后加载趋势并且交易丢失。 WIP here

If trending array empty and trending view gone, deals are not shown,但using invisible are shown

enter image description here

enter image description here

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

FragmentStatePagerAdapter first call to getItem() wrong on Nougat devices

  

这最终与FragmentStatePagerAdapter无关   码。相反,在我的片段中,我从数组中抓取了一个存储的对象   使用我在init中传递给片段的字符串(&#34; id&#34;)。如果我   通过传入对象的位置来抓取存储的对象   阵列,没有问题。仅适用于使用Android 7的设备。

FragmentStatePagerAdapter - getItem

  

FragmentStatePager适配器将加载当前页面和一个页面   任何一边。这就是它同时记录0和1的原因。当你   切换到第2页,它将加载第3页并将第1页保留在内存中。然后   当你到达第4页时,它将不会加载任何内容,因为4加载时   你滚动到3并且没有任何东西。所以那个int   你在getItem()中给出的不是当前的页面   被观看,是被加载到记忆中的那个。希望清除   适合你的事情

这些评论已经确认in this branch and commit

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

enter image description here

enter image description here

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

Increase OffScreenPageLimit so all pages are initialised

Add on page selected/unselected/reselected listener

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

/**
 * 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等。

我会按照建议here

将初始化移到getItem()之外
  

ViewPager默认加载下一页(片段),你不能这样做   由setOffscreenPageLimit(0)更改。但你可以做点什么来破解。   您可以在包含Activity的Activity中实现onPageSelected函数   ViewPager。在下一个片段(你不想加载)中,你   写一个函数让我们说showViewContent()放在哪里   资源消耗初始化代码并且在onResume()方法之前什么都不做。   然后在onPageSelected中调用showViewContent()函数。希望这个   会有所帮助

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

ViewPager.setOffscreenPageLimit(0) doesn't work as expected

  

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

答案 1 :(得分:0)

答案不是很多,但评论时间太长了。

我已经复制了(差不多)你的适配器代码,它完全适合我。我相信我和你一样。我使用相同的布局文件,相同的项目&amp;适用于所有选项卡的适配器我认为您的适配器代码没有问题。

我说&#39;几乎&#39;因为我无法访问您的数据,所以我必须更改一些内容。我将您的LocalDealsDataField模型更改为包含BitmapDrawable&amp;我改变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);

答案 2 :(得分:0)

我查看了你的代码,问题与@ardock解释相同

我想提出的解决方案,

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

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

  2. LocalFragment内填写您要与ViewPager一起使用的片段列表,并将其传递给BottomSectionsPagerAdapter。并从Fragment的{​​{1}}中返回该列表中的getItem(int position)

  3. 将以下代码添加到BottomSectionsPagerAdapter内的LocalFragment

    useSlidingTabViewPager()

    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) { } });` 调用相应的片段initializeRESPECTIVEView()方法,您可以从传递给onTabSelected的列表中获取片段实例