1、缘来
最近看到一个比较有意思的功能,看着像ViewPager的翻页,又像HorizontalScrollview的滑动。如下图所示:
联想到项目中刚做的一个类似的水平滑动,可以借鉴下,如果每次滑动都可以对齐图片边界,用户体验会不会更好点呢?
2、实现
毫无疑问,使用HorizontalScrollview肯定可以实现这样的功能,但是想一下应该需要处理一系列的触摸事件,判断滑动距离,自己去做图片边界对齐等等,想想就头大。上述功能看着像ViewPager的翻页,但是之前使用ViewPager基本都是一个item占满整个一屏,那么可不可以设置ViewPager一屏显示多个item呢?
很容易我们就找到了PagerAdapter的getPageWidth方法。首先,看看源码介绍。
/**
* Returns the proportional width of a given page as a percentage of the
* ViewPager's measured width from (0.f-1.f]
*
* @param position The position of the page requested
* @return Proportional width for the given page position
*/
public float getPageWidth(int position) {
return 1.f;
}
如上注释所示,getPageWidth方法,可以指定page item宽度相对于ViewPager整个宽度的占比,默认为1.0。这也就解释了为什么我们通常使用ViewPager时,每一页都占满整个屏幕了。找到方法,下一步就可以去实现了。
private float pageWidth;
public void computePageWidth(int type) {
int width = screenWidth - ViewUtils.dip2px(context, 30);
int computeWidth;
if (type == 1) {
computeWidth = (screenWidth - ViewUtils.dip2px(context, 40)) / 2;
} else {
computeWidth = (screenWidth - ViewUtils.dip2px(context, 50)) / 3;
}
pageWidth = computeWidth * 1.0f / width;
}
@Override
public float getPageWidth(int position) {
return pageWidth;
}
computePageWidth是根据需求计算pageWidth。这里为啥会这么复杂,需要手动去计算每个item宽度占比呢?这是因为测试时ViewPager使用了setPageMargin方法,该方法可以很方便的指定page item之间的间距。而PagerAdapter的getPageWidth的占比计算并不包括pageMargin部分,因此就会出现问题。比如,假设ViewPager宽度为1000,setPageMargin设置为20,PagerAdapter的pageWidth设置为0.5f,那么每个item的最终宽度为500,而不是期望的(1000-20)/ 2 = 490。
查看ViewPager源码,看看到底是不是没有考虑到pageMargin。在ViewPager的onMeasure中,会分别测量每个item的宽度。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
final int measuredWidth = getMeasuredWidth();
final int maxGutterSize = measuredWidth / 10;
mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
// Children are just made to fill our space.
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
......
// Page views next.
size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
+ ": " + mChildWidthMeasureSpec);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp == null || !lp.isDecor) {
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
}
上述代码中的childWidthSize是ViewPager除去左右padding的内容区域宽度,也就是child的最大宽度,最终child宽度是childWidthSize * lp.widthFactor。这个widthFactor是ViewPager.LayoutParams中定义的宽度因子。在ViewPager的populate方法中,有这样一段代码对每个item的widthFactor进行了赋值操作。
// Check width measurement of current pages and drawing sort order.
// Update LayoutParams as needed.
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.childIndex = i;
if (!lp.isDecor && lp.widthFactor == 0.f) {
// 0 means requery the adapter for this, it doesn't have a valid width.
final ItemInfo ii = infoForChild(child);
if (ii != null) {
lp.widthFactor = ii.widthFactor;
lp.position = ii.position;
}
}
}
可见,lp.widthFactor与ii.widthFactor直接相关。ItemInfo顾名思义就是每个item的一些信息,那么它又是什么时候赋值的呢?继续找,找到了ItemInfo初始化时的代码。
ItemInfo addNewItem(int position, int index) {
ItemInfo ii = new ItemInfo();
ii.position = position;
ii.object = mAdapter.instantiateItem(this, position);
ii.widthFactor = mAdapter.getPageWidth(position);
if (index < 0 || index >= mItems.size()) {
mItems.add(ii);
} else {
mItems.add(index, ii);
}
return ii;
}
很明显了,ItemInfo的widthFactor就等于PagerAdapter的pageWidth!果然,ViewPager没有考虑pageMargin的感受,看来这点设计上还是不够完美。因此,在设置了ViewPager的pageMargin情况下,需要我们手动去计算每个item的宽度占比。如果不想这么计算,可以将item之间的间距直接写在item布局中即可。