Android ViewPager总结
设置缓存页数
最小值为1,不可为0; 想不预加载下一页数据,参见 懒加载 部分。
设置禁止滑动
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
/**
* Created by ykh on 2016/7/11.
*/
public class CustomViewPager extends ViewPager {
private boolean isCanScroll = true;
public CustomViewPager(Context context) {
super(context);
}
public CustomViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setCanScroll(boolean isCanScroll) {
this.isCanScroll = isCanScroll;
}
// 只重写该方法无法禁用
@Override
public boolean onTouchEvent(MotionEvent event) {
return this.isCanScroll && super.onTouchEvent(event);
}
// 还需要重写本方法
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return this.isCanScroll && super.onInterceptTouchEvent(event);
}
}
用法:
不重写onInterceptTouchEvent方法会导致禁止滑动时用户可以慢慢的一点点的划出下一页的一多半页面的情况。
方法具体意义见《Android开发艺术探讨》P141。
懒加载
若不控制预加载:
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
initView();
super.onViewCreated(view, savedInstanceState);
}
若控制预加载: package ykh.viewpagerdemo;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.app.Fragment;
import android.widget.Toast;
/**
* Created by ykh on 2018/6/09.
*/
public abstract class BaseFragment extends Fragment {
/**
* Fragment当前状态是否可见
*/
protected boolean isVisible;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (getUserVisibleHint()) {
isVisible = true;
onVisible();
} else {
isVisible = false;
onInvisible();
}
}
/**
* 可见
*/
protected void onVisible() {
onViewLoad();
}
/**
* 不可见
*/
protected void onInvisible() {
onViewLost();
}
/**
* 延迟加载
* 子类必须重写此方法
*/
protected abstract void onViewLoad();
protected abstract void onViewLost();
}
子Fragment
继承BaseFragment
,需要注意的是onViewLoad
的调用顺序在onViewCreated
之前,也就是说第一次数据加载的时候,可见的页面在onViewCreated
方法中initView
,不可见的页面在onViewLoad中initView
加载数据。实例:
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class ListFragment extends BaseFragment {
private boolean is_init = false;
private Activity activity;
private View layout;
private String name_str;
private TextView name;
private int count = 0;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = this.getActivity();
layout = activity.getLayoutInflater().inflate(R.layout.fragment_todo_list, container, false);
Bundle bundle = this.getArguments();
if (bundle != null) {
name_str = bundle.getString("name", "");
}
return layout;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
if (isVisible) {
initView();
}
super.onViewCreated(view, savedInstanceState);
}
public void initView() {
if (!is_init) {
Logger.i("initView " + name_str);
count++;
name = (TextView) layout.findViewById(R.id.name);
name.setText(name_str + " Pager Loaded Times:" + count);
// 懒加载数据的地方
is_init = true;
}
}
@Override
protected void onViewLoad() {
Logger.i("onViewLoad " + name_str + activity);
if (activity != null) {
initView();
}
}
@Override
protected void onViewLost() {
}
@Override
public void onDestroyView() {
super.onDestroyView();
is_init = false;
}
}
FragmentPagerAdapter
和FragmentStatePagerAdapter
的区别
使用 FragmentPagerAdapter
时,ViewPager
中的所有 Fragment
实例常驻内存,当 Fragment 变得不可见时仅仅是视图结构的销毁,即调用了 onDestroyView
方法。由于 FragmentPagerAdapter
内存消耗较大,所以适合少量静态页面的场景。
使用 FragmentStatePagerAdapter
时,当 Fragment
变得不可见,不仅视图层次销毁,实例也被销毁,即调用了onDestroyView
和 onDestroy
方法,仅仅保存 Fragment
状态。相比而言, FragmentStatePagerAdapter
内存占用较小,所以适合大量动态页面,比如我们常见的新闻列表类应用。
替换ViewPager中Fragment无效问题
FragmentPagerAdapter内置缓存导致替换ViewPager中Fragment无效问题
遍历FragmentPagerAdapter
源代码可见instantiateItem
函数,其根据getItemId(position)
函数来得到itemId
,再根据根据makeFragmentName(container.getId(), itemId)
来获取itemId
对应的name
,再根据findFragmentByTag(name)
来判断当前fragment
是否已经存在,已存在则直接使用,不存在则调用getItem(position)
函数来重新生成。
@SuppressWarnings("ReferenceEquality")
@Override
public Object instantiateItem(ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
return fragment;
}
搜索得到的解决方法
- 方法一:重写
adapter
的getItemPosition()
方法如下:
当调用notifyDataSetChanged()
的时候。ViewPager
会remove
掉全部的view
,然后又一次去载入。可行,可是效率低。
详见此处
- 方法二:暴力删除
fragment
,方法如下:
List<Fragment> fragments = getSupportFragmentManager(0.getFragments();
for(int i=0;i<fragments.size();i++){
getSupportFragmentManager().beginTransaction().remove(fragments.get(i));
}
-
方法三:重写
FragmentPagerAdapter
的instantiateItem
方法。 -
方法四:重写
getItemId
如下:
FragmentPagerAdapter里在根据getItemId(int position)来判断当前position里Fragment是否存在,如果存在,则不会创建亦不会更新,那么要让FragmentPagerAdapter的更新生效,那在getItemId(int)里根据数据返回一个唯一的数据ID,当FragmentPagerAdapter更新时,数据ID改变了,那么Fragment就会调用getItem(int)去获取新Fragment,达到更新效果(来源)
- 方法5:为每个
Fragment
设置tag(推荐)
摘抄如下:
My approach is to use the setTag() method for any instantiated view in the instantiateItem() method. So when you want to change the data or invalidate the view that you need, you can call the findViewWithTag() method on the ViewPager to retrieve the previously instantiated view and modify/use it as you want without having to delete/create a new view each time you want to update some value.
具体实现可见此博客,核心代码如下:
public class FragmentViewPagerAdapter extends FragmentPagerAdapter {
private FragmentManager mFragmentManager;
private List<String> mDatas;
private List<String> tagList = new ArrayList<String>();
public FragmentViewPagerAdapter(FragmentManager fm, List<String> datas) {
super(fm);
this.mFragmentManager = fm;
this.mDatas = datas;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
tagList.add(makeFragmentName(container.getId(), getItemId(position))); //把tag存起来
return super.instantiateItem(container, position);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
tagList.remove(makeFragmentName(container.getId(), getItemId(position)));//把tag删掉
}
@Override
public Fragment getItem(int position) {
String url = mDatas.get(position);
WebViewFragmentV4 webview = new WebViewFragmentV4(url);//本文測试的Fragment是一个WebViewFragment
return webview;
}
@Override
public int getCount() {
if (mDatas == null) {
return 0;
} else {
return mDatas.size();
}
}
public void update(List<String> datas) {
this.mDatas = datas;
notifyDataSetChanged();//并不能起到更新Fragment内容的作用。
}
public void update(int position) {//这个事真正的更新Fragment的内容
WebViewFragmentV4 fragment = (WebViewFragmentV4) mFragmentManager.findFragmentByTag(tagList.get(position));
if (fragment == null) {
return;
}
fragment.update();
}
private static String makeFragmentName(int viewId, long id) {
return "android:switcher:" + viewId + ":" + id;
}
}
最后使用的解决方案
写博客的时候遇到的具体问题场景为,一个ViewPager
中有两个Fragment
,需要动态的替换(而不是更新其内容)第二个Fragment
为新的Fragment
,但是又不愿意重新加载第一个,使用的adapter
如下:
private class DynamicPagerAdapter extends FragmentPagerAdapter {
private ArrayList<Fragment> registeredFragments;
private List<Integer> hashList;
private DynamicPagerAdapter(FragmentManager fm) {
super(fm);
this.registeredFragments = new ArrayList<>();
hashList = new ArrayList<>();
}
public int getItemPosition(Object object) {
int index = getIndexByTag(object.hashCode());
if (index >= 0) {
hashList.remove(index);
return POSITION_NONE;
} else {
return POSITION_UNCHANGED;
}
}
public long getItemId(int position) {
return registeredFragments.get(position).hashCode();
}
@Override
public Fragment getItem(int position) {
return registeredFragments.get(position);
}
@Override
public int getCount() {
return registeredFragments.size();
}
public void add(Fragment fragment) {
registeredFragments.add(fragment);
hashList.add(fragment.hashCode());
notifyDataSetChanged();
}
public void remove(int position) {
registeredFragments.remove(position);
notifyDataSetChanged();
}
private int getIndexByTag(int hashCode) {
for (int i = 0; i < hashList.size(); i++) {
if (hashList.get(i) == hashCode) {
return i;
}
}
return -1;
}
}
主要思路:记录所有新增的Fragment
的hashCode
,在getItemPosition
时,根据Fragment
判断该对象是否已经被记录,若未记录则返回POSITION_NONE
让其被重新加载,否则返回POSITION_UNCHANGED
以使用缓存。
ViewPager中使用相同的Fragment
页面不更新,只显示第一个Fragment
绑定控件时应使用layout
查找而不是activity
查找,应该为:
而不是: