在 ViewPager 中使用 SwipeRefreshLayout 呈现的列表的冻结快照

Frozen snapshot of list rendered with SwipeRefreshLayout in ViewPager

考虑一个 Android activity,它是 android.support.v4.app.FragmentActivity 的子class。它包含几个由 FragmentStatePagerAdapter 管理的页面。每页包含 android.support.v4.widget.SwipeRefreshLayout 包裹的 ListView。以下屏幕截图显示了此 activity.

的初始状态

现在,我向下滑动以刷新第 0 页上的列表。出现刷新动画。现在,让我们在第 2 页向右滑动,然后返回第 0 页。

刷新动画仍然可见。这对我来说似乎很奇怪,因为第 0 页被销毁并再次创建。此外,如果我向上滑动(尝试滚动到第 0 页列表的末尾),页面上会呈现一些奇怪的层,它似乎包含页面被销毁时的旧状态。

我不知道为什么会这样,也不知道如何解决。感谢您的帮助。

这是一个最小应用程序的源代码,它包含 3 个布局文件、一个 activity class、一个片段 class、一个清单文件和一个构建脚本。

swipeandroid/src/main/res/layout/service_schedule_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/servicePager"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.view.PagerTabStrip
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="top"/>

</android.support.v4.view.ViewPager>

swipeandroid/src/main/res/layout/service_schedule_tab_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/serviceRefreshLayout"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/timeSlots"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</android.support.v4.widget.SwipeRefreshLayout>

swipeandroid/src/main/res/layout/service_schedule_row.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/time"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"/>

</RelativeLayout>

swipeandroid/src/main/java/com/swipetest/ServiceScheduleActivity.java

package com.swipetest;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;

public class ServiceScheduleActivity extends FragmentActivity {
    @Override
    protected void onCreate(final Bundle inState) {
        // call super class
        super.onCreate(inState);

        // set content
        setContentView(R.layout.service_schedule_activity);

        // set up service pager
        ((ViewPager) findViewById(R.id.servicePager)).setAdapter(new ServiceFragmentPagerAdapter());
    }

    private class ServiceFragmentPagerAdapter extends FragmentStatePagerAdapter {
        public ServiceFragmentPagerAdapter() {
            super(getSupportFragmentManager());
        }

        @Override
        public int getCount() {
            return 5;
        }

        @Override
        public Fragment getItem(final int position) {
            return ServiceScheduleTabFragment.newInstance(position);
        }

        @Override
        public CharSequence getPageTitle(final int position) {
            return String.format("page %d", position);
        }
    }
}

swipeandroid/src/main/java/com/swipetest/ServiceScheduleTabFragment.java

package com.swipetest;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class ServiceScheduleTabFragment extends Fragment {
    private static final String TAB_POSITION_ARGUMENT = "tabPosition";

    public static ServiceScheduleTabFragment newInstance(final int tabPosition) {
        // prepare arguments
        final Bundle arguments = new Bundle();
        arguments.putInt(TAB_POSITION_ARGUMENT, tabPosition);

        // create fragment
        final ServiceScheduleTabFragment fragment = new ServiceScheduleTabFragment();
        fragment.setArguments(arguments);
        return fragment;
    }

    @Override
    public View onCreateView(final LayoutInflater layoutInflater, final ViewGroup parent, final Bundle inState) {
        // create fragment root view
        final View rootView = layoutInflater.inflate(R.layout.service_schedule_tab_fragment, parent, false);

        // initialize time slot list view
        final ListView timeSlotListView = (ListView) rootView.findViewById(R.id.timeSlots);
        timeSlotListView.setAdapter(new TimeSlotListAdapter());

        // return fragment root view
        return rootView;
    }

    private int getTabPosition() {
        return getArguments().getInt(TAB_POSITION_ARGUMENT);
    }

    private static class TimeSlotRowViewHolder {
        private final TextView timeView;

        public TimeSlotRowViewHolder(final View rowView) {
            timeView = (TextView) rowView.findViewById(R.id.time);
        }

        public TextView getTimeView() {
            return timeView;
        }
    }

    private class TimeSlotListAdapter extends BaseAdapter {

        @Override
        public int getCount() {
            return 80;
        }

        @Override
        public Object getItem(final int position) {
            return position;
        }

        @Override
        public long getItemId(final int position) {
            return position;
        }

        @Override
        public View getView(final int position, final View convertView, final ViewGroup parent) {
            // reuse or create row view
            final View rowView;
            if (convertView != null) {
                rowView = convertView;
            } else {
                rowView = LayoutInflater.from(getContext()).inflate(R.layout.service_schedule_row, parent, false);
                rowView.setTag(new TimeSlotRowViewHolder(rowView));
            }

            // fill data
            final TimeSlotRowViewHolder rowViewHolder = (TimeSlotRowViewHolder) rowView.getTag();
            rowViewHolder.getTimeView().setText(String.format("tab %d, row %d", getTabPosition(), position));
            return rowView;
        }
    }
}

swipeandroid/src/main/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.swipetest"
          android:versionCode="1"
          android:versionName="1.0">

    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="23"/>

    <application
        android:name="android.app.Application"
        android:label="Swipe Test"
        android:allowBackup="false">

        <activity
            android:name="com.swipetest.ServiceScheduleActivity">

            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>

        </activity>

    </application>

</manifest>

swipeandroid/build.gradle

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
    }
}

apply plugin: 'com.android.application'

repositories {
    mavenCentral()
}

dependencies {
    compile "com.android.support:support-v4:23.1.1"
}

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"
    buildTypes {
        debug {
            debuggable true
            minifyEnabled false
        }
    }
}

Try to clear the list and then load the new data on swipe refresh.

如评论中所述。

最后,我发现了一个实施起来非常简单的解决方法并解决了这个问题。

只需将 SwipeRefreshLayout 包裹起来,例如通过 FrameLayoutViewPager 似乎不喜欢 SwipeRefreshLayout 作为页面片段视图层次结构的根。

无论如何,这个问题看起来像是 Android 支持库中的错误。看 https://code.google.com/p/android/issues/detail?id=194957

我也尝试过其他无效的解决方法:

  • FragmentStatePagerAdapter 替换为 `FragmentPagerAdapter 将无济于事。
  • onPause 中调用 mySwipeRefreshLayout.setRefreshing(false) 没有帮助。
  • 修改 FragmentStatePagerAdapter 使其不记住已销毁页面的状态将无济于事。

以下变通办法可以以某种方式提供帮助,但无论如何我更喜欢建议的 SwipeRefreshLayout 包装:

  • 清除 SwipeRefreshLayout.OnRefreshListener#onRefresh() 中的数据(以便 ListView 不包含任何行)仅当 ListView 具有透明背景时才有帮助。不需要的快照可能仍在第一页上呈现,但它是完全透明的。
  • 可以禁止刷新动画时分页。但是,这需要一些努力,因为您必须同时对 ViewPagerPagerTabStrip 进行子类化,并防止在子类中出现适当的 UI 事件。
  • 当然,你可以完全避免在ViewPager中使用SwipeRefreshLayout,但这似乎没有必要。