文章

ViewPager基础

ViewPager基础

ViewPager 基础用法

ViewPager smoothScroll 速度控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
 * 通过反射来修改 ViewPager的mScroller属性
 */
try {
  Class clazz=Class.forName("android.support.v4.view.ViewPager");
  Field f=clazz.getDeclaredField("mScroller");
  FixedSpeedScroller fixedSpeedScroller=new FixedSpeedScroller(this,new LinearOutSlowInInterpolator());
  fixedSpeedScroller.setmDuration(2000);
  f.setAccessible(true);
  f.set(mViewPager,fixedSpeedScroller);
} catch (Exception e) {
  e.printStackTrace();
}


/**
 * 利用这个类来修正ViewPager的滑动速度
 * 我们重写 startScroll方法,忽略传过来的 duration 属性
 * 而是采用我们自己设置的时间
 */
public class FixedSpeedScroller extends Scroller {

  public int mDuration=1500;
  public FixedSpeedScroller(Context context) {
    super(context);
  }

  public FixedSpeedScroller(Context context, Interpolator interpolator) {
    super(context, interpolator);
  }

  public FixedSpeedScroller(Context context, Interpolator interpolator, boolean flywheel) {
    super(context, interpolator, flywheel);
  }

  @Override public void startScroll(int startX, int startY, int dx, int dy) {
    startScroll(startX,startY,dx,dy,mDuration);
  }

  @Override public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    //管你 ViewPager 传来什么时间,我完全不鸟你
    super.startScroll(startX, startY, dx, dy, mDuration);
  }

  public int getmDuration() {
    return mDuration;
  }

  public void setmDuration(int duration) {
    mDuration = duration;
  }
}

ViewPager 中的 setCurrentItem 失效

在 onCreate 中给 ViewPager 设置进入哪个 Fragment 页面时 setCurrentItem() 未起作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        @SuppressWarnings("deprecation")
        @SuppressLint("NewApi")
        @Override
        public void onGlobalLayout() {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                getViewTreeObserver().removeGlobalOnLayoutListener(this);
            } else {
                getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
            currentPosition = pager.getCurrentItem();
            currentPositionOffset = 0f;
            scrollToChild(currentPosition, 0);
        }
    });

ViewPager 的 wrap_content 无效

原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ViewPager
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // For simple implementation, our internal size is always 0.
    // We depend on the container to specify the layout size of
    // our view.  We can't really know what it is since we will be
    // adding and removing different arbitrary views and do not
    // want the layout to change as this happens.
    // 设置测量大小为默认的大小
    setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
            getDefaultSize(0, heightMeasureSpec));

    final int measuredWidth = getMeasuredWidth();
    final int maxGutterSize = measuredWidth / 10;
    mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);

    // 设置child的宽高,大小为ViewPager的测量大小减去其内边距
    int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
    int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

    // ---忽略Decor views 的测量---
    
    // 设置child view的 MeasureSpec
    mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
    mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);

    // ---忽略其他代码

    // Page views next.
    size = getChildCount();
    for (int i = 0; i < size; ++i) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            // ---忽略其他代码
             
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (lp == null || !lp.isDecor) {
                final int widthSpec = MeasureSpec.makeMeasureSpec(
                        (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
                // 测量child 的大小
                child.measure(widthSpec, mChildHeightMeasureSpec);
            }
        }
    }
}

ViewPager 在 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法一开始就对自身宽高进行了默认设置,在此之前并没有进行 child view 的测量,故而当高度设置为 “wrap_content” 时不会去匹配 child view 的高度。

在测量完自己之后,取得测量的宽高减去内边距后,设置为 child view 的宽高,而后再生成 MeasureSpec,并以此来对 child view 进行测量。这也就解释了为什么明明设置了 child view 的宽高,但是并不生效,反而去匹配父布局的大小。

错误解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class WrapContentHeightViewPager extends ViewPager {
    public WrapContentHeightViewPager(@NonNull Context context) {
        super(context);
    }

    public WrapContentHeightViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int height = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            int h = child.getMeasuredHeight();
            if (h > height) height = h;
        }

        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

指定 height 为具体的数值时,显示不对。

1
child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));

这一行,高度的 MeasureSpec 指定大小是 0,mode 为 MeasureSpec.UNSPECIFIED。我们知道一个 view 的大小受自身的 LayoutParams 和父 view 的 MeasureSpec 的双重限制,还需要加上 LayoutParams 的设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class WrapContentHeightViewPager extends ViewPager {
    public WrapContentHeightViewPager(@NonNull Context context) {
        super(context);
    }

    public WrapContentHeightViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int height = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // child 的大小受自身的LayoutParams 和父view 的MeasureSpec 的双重限制!测量高度需要同时考虑这两个因素。
            ViewGroup.LayoutParams lp = child.getLayoutParams();
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(lp.height, heightMeasureSpec));
            int h = child.getMeasuredHeight();
            if (h > height) height = h;
        }

        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

ListView 头部添加 ViewPager 事件不响应,ListView 下面的 ViewPager 事件不响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 自定义ViewPager,重写dispatchTouchEvent()方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
        //请求父容器不拦截事件,响应viewpager的左右滑动
        getParent().requestDisallowInterceptTouchEvent(true);
        downX = ev.getX();
        downY = ev.getY();
        break;
    case MotionEvent.ACTION_MOVE:
        float moveX = ev.getX();
        float moveY = ev.getY();
        if (Math.abs(downX - moveX) > Math.abs(downY - moveY)) {
            //请求父容器不可以拦截事件,不响应viewpager的左右滑动,响应listview的上下滑动
            getParent().requestDisallowInterceptTouchEvent(true);
        } else {
            //请求拦截事件
            getParent().requestDisallowInterceptTouchEvent(false);
        }
        break;
    case MotionEvent.ACTION_UP:
        break;
    }
    return super.dispatchTouchEvent(ev);
}  

定制 ViewPager

不会滑动的 ViewPager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class NoSlidingViewPager extends LazyViewPager {
    public NoSlidingViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public NoSlidingViewPager(Context context) {
        super(context);
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return false;
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
//        return super.onInterceptTouchEvent(ev);
        //FIXME:返回false,看看ViewPager事件是怎么拦截的
        return false;
    }
}

不会预加载页面的 ViewPager,懒加载的 ViewPager

ViewPager 中本来充满善意的预加载就有点令人不爽了。我们能做的就是屏蔽掉 ViewPager 的预加载机制。虽然 ViewPager 中提供的有
setOffscreenPageLimit() 来控制其预加载的数目,但是当设置为 0 后我们发现其根本没效果,这个的最小值就是 1,也就是你只能最少前后各预加载一页。

方案 1:修改源码

直接将 ViewPager 中的将 private static final int DEFAULT_OFFSCREEN_PAGES = 0;//默认的加载页面,ViewPager是1个,所以会加载两个Fragment

缺点:

应用太受限制,比如使用 ViewPagerIndicator 时需要传入 ViewPager 对象

方案 2:Fragment 的懒加载

缺点:
不是真正意义上的懒加载,还是会创建 3 个 Fragment 实例

方案 3:LazyViewPager

开源方案https://github.com/lianghanzhen/LazyViewPager

PageTransformer

ViewPager.PageTransformer 介绍

实现 ViewPager 切换动画

1
2
3
4
5
6
7
8
9
10
11
12
public interface PageTransformer {
    /**
        * Apply a property transformation to the given page.
        *
        * @param page Apply the transformation to this page
        * @param position Position of page relative to the current front-and-center
        * position of the pager. 0 is front and center. 1 is one full
        * page position to the right, and -1 is one page position to the left.
        */
    void transformPage(View page, float position);
}

比如当前页是 0 页,从第 0 页滑动到第 1 页,默认情况下 (setOffscreenPageLimit 为默认值 1):

ViewPager 中一个页面显示多个 ViewPager 的 Item

方式 1-clipChildren

  1. 在父容器和 ViewPager 中都添加上 clipChildren 属性
  2. 然后给 ViewPager 设置左右两个 margin,使其不致于把整个屏幕占满
  3. 给 ViewPager 设置 setPageMargin() 和 setOffscreenPageLimit()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:orientation="vertical"
        android:clipChildren="false"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <android.support.v4.view.ViewPager
            android:layout_marginLeft="50dp"
            android:layout_marginRight="50dp"
            android:visibility="visible"
            android:clipChildren="false"
            android:layout_width="match_parent"
            android:id="@+id/vp"
            android:layout_height="wrap_content">
    </android.support.v4.view.ViewPager>

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:orientation="vertical"
        android:clipChildren="false"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <android.support.v4.view.ViewPager
            android:layout_marginLeft="50dp"
            android:layout_marginRight="50dp"
            android:visibility="visible"
            android:clipChildren="false"
            android:layout_width="match_parent"
            android:id="@+id/vp"
            android:layout_height="wrap_content">
    </android.support.v4.view.ViewPager>

方式 2:clipToPadding

ViewPager Ref

本文由作者按照 CC BY 4.0 进行授权