在 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
包裹起来,例如通过 FrameLayout
。 ViewPager
似乎不喜欢 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
具有透明背景时才有帮助。不需要的快照可能仍在第一页上呈现,但它是完全透明的。
- 可以禁止刷新动画时分页。但是,这需要一些努力,因为您必须同时对
ViewPager
和 PagerTabStrip
进行子类化,并防止在子类中出现适当的 UI 事件。
- 当然,你可以完全避免在
ViewPager
中使用SwipeRefreshLayout
,但这似乎没有必要。
考虑一个 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
包裹起来,例如通过 FrameLayout
。 ViewPager
似乎不喜欢 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
具有透明背景时才有帮助。不需要的快照可能仍在第一页上呈现,但它是完全透明的。 - 可以禁止刷新动画时分页。但是,这需要一些努力,因为您必须同时对
ViewPager
和PagerTabStrip
进行子类化,并防止在子类中出现适当的 UI 事件。 - 当然,你可以完全避免在
ViewPager
中使用SwipeRefreshLayout
,但这似乎没有必要。