利用ViewPage的PagerTransformer定制页面切换效果 1-6
1. 简述
你是不是觉得 ViewPager 默认的切换效果有些平淡?其实,我们可以定制 ViewPager 的页面切换效果。定制 ViewPager 的页面切换效果,只需用到 ViewPager 的一个方法setPageTransformer(boolean reverseDrawingOrder, @Nullable PageTransformer transformer),实现一个接口 PageTransformer 。
2. PageTransformer
PageTransformer 是 ViewPager 内部一个接口,源码如下:
/**
* A PageTransformer is invoked whenever a visible/attached page is scrolled.
* This offers an opportunity for the application to apply a custom transformation
* to the page views using animation properties.
* 每当滚动到 可见/附属 页面时,都会调用 PageTransformer 。
* 这为应用程序提供了使用动画属性对页面视图应用自定义转换的机会。
* <p>As property animation is only supported as of Android 3.0 and forward,
* setting a PageTransformer on a ViewPager on earlier platform versions will
* be ignored.</p>
*/
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.
* 页面相对于当前正中位置的位置。0代表正中。1 是右边的一个完整页面位置,
* -1 是左边的一个页面位置。
*/
void transformPage(@NonNull View page, float position);
}
从源码注释我们可以看出, PageTransformer 就是为自定义页面切换效果而生的。
使用 PageTransformer 需要实现方法 void transformPage(@NonNull View page, float position),这个方法有两个参数,两个参数是对应关系,position 表示页面相对于正中(0)的位置,0代表正中,1 是右边的一个完整页面位置,-1 是左边的一个页面位置。
如下图所示,ViewPager 页面滑动过程中,屏幕中显示 2 个页面,即左边页面(绿色)的一部分和右边页面(蓝色)的一部分。以虚线和页面左边缘作为位置参考,那么左边页面位置在 (-1, 0) 区间,右边页面位置在 (0, 1) 区间。它们的值是:position左 = (x左 - x0) / width,position右 = (x右 - x0)/ width。
一般情况下我们最多只能看到如上所示两个页面,所以多数情况下,我们可以把 position 分为 3 段。
- (-Infinity, -1) :左边不可见页面。
- [-1, 1]:中间可见页面,其中 [-1, 0) 表示左边可见页面,[0, 1] 表示右边可见页面。
- (1, +Infinity) :右边不可见页面。当页面的 position = 0,此页面正中显示。
可以调用 ViewPager 的 setOffscreenPageLimit(int) 方法设置了离屏缓存页面数量(空闲时,当前页面左右保留页面数量)。
如果想在 ViewPager 中显示多个页面,可以:
- 调用 setOffscreenPageLimit(int) 设置离屏缓存页面数量 大于 1;
- 给 ViewPager 外层控件以及 ViewPager 都设置 android:clipChildren="false";
- 给 ViewPager 设置 适当的 leftMargin 和 rightMargin 。
3. 例子和效果
1、手风琴效果其实就是水平缩放效果。在页面滑动时,
左边页面 position < 0,右边页面 position > 0;
左边页面以页面右边缘为缩放中心,右边页面以左边缘为缩放中心。
代码如下所示:
/**
* 手风琴效果(水平方向缩放)
*/
public class AccordionTransformer implements ViewPager.PageTransformer {
@Override
public void transformPage(@NonNull View page, float position) {
if (position < 0f) {
page.setPivotX(page.getWidth());
page.setScaleX(1f + position * 0.5f);
} else if (position < 1f) {
page.setPivotX(0f);
page.setScaleX(1f - position * 0.5f);
}
}
}
2、下弧形效果
实现此效果需以页面下边缘某一点为旋转中心旋转:
position < -1 时,旋转到最大角度,旋转中心为右下角;
-1 < position < 0 时,position 越靠近 0 ,旋转角度越小,旋转中心向下边缘中心靠拢;
0 <= position <= 1 时,position 越靠近 0 ,旋转角度越小,旋转中心向下边缘中心靠拢;
position > 1 时,旋转到最大角度,旋转中心为左下角。
代码如下所示:
/**
* 下弧形切换效果
*/
public class ArcDownTransformer implements ViewPager.PageTransformer {
private static final float DEF_MAX_ROTATE = 12.0f;
private float mMaxRotate = DEF_MAX_ROTATE;
@Override
public void transformPage(@NonNull View page, float position) {
page.setPivotY( page.getHeight());
if (position < -1f) {//[-Infinity, -1)
page.setRotation(-mMaxRotate);
page.setPivotX(page.getWidth());
} else if (position <= 1f) {//[-1, 1]
if (position < 0f) {//[-1, 0)
page.setRotation(mMaxRotate * position);
page.setPivotX(page.getWidth() * (0.5f - 0.5f * position));
} else { //[0, 1]
page.setRotation(mMaxRotate * position);
page.setPivotX(page.getWidth() * (0.5f - 0.5f * position));
}
} else {//(1, +Infinity]
page.setRotation(mMaxRotate);
page.setPivotX(0f);
}
}
}
3、上弧形效果
与下弧形相反,旋转中心以上边缘某一点:
- position < -1 时,旋转到最大角度,旋转中心为右下角;
- -1 < position < 0 时,position 越靠近 0 ,旋转角度越小,旋转中心向上边缘中心靠拢;
- 0 <= position <= 1 时,position 越靠近 0 ,旋转角度越小,旋转中心向上边缘中心靠拢;
- position > 1 时,旋转到最大角度,旋转中心为左下角。
代码如下:/** * 上弧形切换效果 */ public class ArcUpTransformer implements ViewPager.PageTransformer { private static final float DEF_MAX_ROTATE = 12.0f; private float mMaxRotate = DEF_MAX_ROTATE; @Override public void transformPage(@NonNull View page, float position) { page.setPivotY(0f); if (position < -1f) {//[-Infinity, -1) page.setRotation(mMaxRotate); page.setPivotX(page.getWidth()); } else if (position <= 1f) {//[-1, 1] if (position < 0f) {//[-1, 0) page.setRotation(-mMaxRotate * position); page.setPivotX(page.getWidth() * (0.5f - 0.5f * position)); } else { //[0, 1] page.setRotation(-mMaxRotate * position); page.setPivotX(page.getWidth() * (0.5f - 0.5f * position)); } } else {//(1, +Infinity] page.setRotation(-mMaxRotate); page.setPivotX(0f); } } }
4、立方翻转-外
其实是绕 Y 轴旋转,再加上缩放效果。绕 Y 轴旋转,用到了 View 的 setRotationY(float) 方法,此方法可以设置绕 Y 轴的旋转角度。
- position < -1,逆时针旋转到最大角度,旋转中心为页面右边缘;
- -1 <= position < 0,旋转中心为页面右边缘,position 越靠近 0,旋转角度越小,页面先缩小后放大,缩放值是关于 position 开口向上 对称线为 position = -0.5 的抛物线;
- 0 <= position <= 1,旋转中心为左边缘,position 越靠近 0,旋转角度越小,页面先缩小后放大,缩放值是关于 position 开口向上 对称线为 position = 0.5 的抛物线;
- position > 1,顺时针旋转到最大角度,旋转中心为左边缘。
引入抛物线计算缩放值,是为了让页面在滑动到一半(position 为 -0.5 和 0.5)时,缩放到最小。
需要注意 的是镜头距离,即图像与屏幕距离,距离较小时,旋转时会有较大的失真,效果很差,需要设置一下镜头距离(Camera Distance,参考 View#setCameraDistance(float))。
代码如下:
/**
* 立方体翻转效果
*/
public class CubicOverturnTransformer implements ViewPager.PageTransformer {
public static final float DEFAULT_MAX_ROTATION = 60f;
public static final float DEF_MIN_SCALE = 0.86f;
/**
* 最大旋转角度
*/
private float mMaxRotation = DEFAULT_MAX_ROTATION;
/**
* 最小缩放
*/
private float mMinScale = DEF_MIN_SCALE;
public CubicOverturnTransformer() {
this(DEFAULT_MAX_ROTATION);
}
public CubicOverturnTransformer(float maxRotation) {
this(maxRotation, DEF_MIN_SCALE);
}
public CubicOverturnTransformer(float maxRotation, float minScale) {
mMaxRotation = maxRotation;
this.mMinScale = minScale;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override
public void transformPage(@NonNull View page, float position) {
page.setPivotY(page.getHeight() / 2f);
float distance = getCameraDistance();
page.setCameraDistance(distance);//设置 View 的镜头距离,可以防止旋转大角度时出现图像失真或不显示。
if (position < -1) { // [-Infinity,-1)
page.setRotationY(-mMaxRotation);
page.setPivotX(page.getWidth());
} else if (position <= 1) { // [-1,1]
page.setRotationY(position * mMaxRotation);
if (position < 0) {//[0,-1]
page.setPivotX(page.getWidth());
float scale = DEF_MIN_SCALE + 4f * (1f - DEF_MIN_SCALE) * (position + 0.5f) * (position + 0.5f);
page.setScaleX(scale);
page.setScaleY(scale);
} else {//[1,0]
page.setPivotX(0);
float scale = DEF_MIN_SCALE + 4f * (1f - DEF_MIN_SCALE) * (position - 0.5f) * (position - 0.5f);
page.setScaleX(scale);
page.setScaleY(scale);
}
} else { // (1,+Infinity]
page.setRotationY(mMaxRotation);
page.setPivotX(0);
}
}
/**
* 获得镜头距离(图像与屏幕距离)。参考{@link View#setCameraDistance(float)},小距离表示小视角,
* 大距离表示大视角。这个距离较小时,在 3D 变换(如围绕X和Y轴的旋转)时,会导致更大的失真。
* 如果改变 rotationX 或 rotationY 属性,使得此 View 很大 (超过屏幕尺寸的一半),则建议始终使用
* 大于此时图高度 (X 轴旋转)或 宽度(Y 轴旋转)的镜头距离。
* @return 镜头距离 distance
*
* @see {@link View#setCameraDistance(float)}
*/
private float getCameraDistance() {
DisplayMetrics displayMetrics = VpsApplication.getAppContext().getResources().getDisplayMetrics();
float density = displayMetrics.density;
int widthPixels = displayMetrics.widthPixels;
int heightPixels = displayMetrics.heightPixels;
return 1.5f*Math.max(widthPixels, heightPixels)*density;
}
}
使用:
mPageTransformer = new CubicOverturnTransformer(90f, 0.6f);
mVpImgs.setPageTransformer(reverseDrawingOrder, mPageTransformer);
5、立方翻转-内
与上一个效果是同一套代码,绕 Y 轴旋转方向相反,使用时,让 最大旋转角度小于 0 即可,如下代码所示,最大旋转角度改为 -90f 就是内部翻转:
使用:
mPageTransformer = new CubicOverturnTransformer(-90f, 0.6f);
mVpImgs.setPageTransformer(reverseDrawingOrder, mPageTransformer);
最大旋转角度 90 度,最小缩放到 0.6 。
6、下沉效果
特殊的缩放效果,有最小缩放值,页面缩到最小值不再缩小,同时有透明度的变化(可以去掉透明度变化)。
旋转中心位置的调整主要是为了调整页面间隙。
/**
* 下沉效果
*/
public class DipInTransformer implements ViewPager.PageTransformer {
private static final float MIN_SCALE = 0.85f;
private float mMinScale = MIN_SCALE;
private static final float MIN_ALPHA = 0.5f;
private float mMinAlpha = MIN_ALPHA;
@Override
public void transformPage(@NonNull View page, float position) {
Log.i("DipInTransformer", "transformPage: id = " + page.getId() + ", position = " + position);
int pageWidth = page.getWidth();
int pageHeight = page.getHeight();
// page.setPivotX(pageWidth * 0.5f);
page.setPivotY(pageHeight * 0.5f);
if (position < -1f) {//(-Infinity, -1]
page.setAlpha(mMinAlpha);
page.setScaleX(mMinScale);
page.setScaleY(mMinScale);
page.setPivotX(pageWidth*1f);
} else if (position <= 1f) {//(-1, 1)
float scaleFactor = Math.max(mMinScale, 1 - Math.abs(position));
page.setScaleX(scaleFactor);
page.setScaleY(scaleFactor);
if (position < 0) {
page.setPivotX(pageWidth * (0.5f + 0.5f * scaleFactor));
} else {
page.setPivotX(pageWidth * (0.5f - 0.5f * scaleFactor));
}
page.setAlpha(mMinAlpha + (scaleFactor - mMinScale) / (1f - mMinScale) * (1f - mMinAlpha));
} else {//(1, +Infinity)
page.setAlpha(mMinAlpha);
page.setScaleX(mMinScale);
page.setScaleY(mMinScale);
page.setPivotX(pageWidth * 0f);
}
}
}
👍👍👍