一直对android桌面的翻页效果比较感兴趣,这两天有空所以去研究了下它的实现原理。
Launcher3 的页面分为两块:工作区(workspace)和主菜单(APPS_CUSTOMIZE),这两块基本功能差不多,都是继承于PagedView,工作区的翻页效果是普通的平滑过渡,没什么特殊效果。主菜单页则采用了类似于扑克牌的层叠效果,本文重点研究其实现,最后顺便修改成其它效果。
开始一直以为是由动画效果生成的,结果走了不少弯路,后来发现是Scroller滚动时不断利用重载dispatchDraw绘制子页生成的一个动画效果。关键代码如下:
@Override
protected void dispatchDraw(Canvas canvas) {
int halfScreenSize = getViewportWidth() / 2; // 270
// mOverScrollX is equal to getScrollX() when we're within the normal
// scroll range.
// Otherwise it is equal to the scaled overscroll position.
int screenCenter = mOverScrollX + halfScreenSize;
// 当位置为变化或强制滑动时
if (screenCenter != mLastScreenCenter || mForceScreenScrolled) {
// set mForceScreenScrolled before calling screenScrolled so that
// screenScrolled can
// set it for the next frame
mForceScreenScrolled = false;
//绘制翻页效果的关键位置,下面将重点展开详解。
screenScrolled(screenCenter);
mLastScreenCenter = screenCenter;
}
// Find out which screens are visible; as an optimization we only call
// draw on them
final int pageCount = getChildCount();
if (pageCount > 0) {
getVisiblePages(mTempVisiblePagesRange);
final int leftScreen = mTempVisiblePagesRange[0];
final int rightScreen = mTempVisiblePagesRange[1];
if (leftScreen != -1 && rightScreen != -1) {
final long drawingTime = getDrawingTime();
// Clip to the bounds
canvas.save();
canvas.clipRect(getScrollX(), getScrollY(), getScrollX()
+ getRight() - getLeft(), getScrollY() + getBottom()
- getTop());
// Draw all the children, leaving the drag view for last
//绘制所有子页,拖动页最后绘制
for (int i = pageCount - 1; i >= 0; i--) {
final View v = getPageAt(i);
if (v == mDragView)
continue;
if (mForceDrawAllChildrenNextFrame
|| (leftScreen <= i && i <= rightScreen && shouldDrawChild(v))) {
drawChild(canvas, v, drawingTime);
}
// Draw the drag view on top (if there is one)
if (mDragView != null) {
drawChild(canvas, mDragView, drawingTime);
}
mForceDrawAllChildrenNextFrame = false;
canvas.restore();
}
}
}
}
当Scroll位置有变化时,就进入screenScrolled这个类,对子页进行缩放,改变透明度,转换角度,移动等,不断变化就组合成了我们看到的层叠效果。都是view自带的一些类,没有涉及复杂的动画效果。
// launcher3\AppsCustomizePagedView.java
// In apps customize, we have a scrolling effect which emulates pulling cards off of a stack.
@Override
protected void screenScrolled(int screenCenter) {
// 右到左布局,一般中东一些语言才采用,这里为false
final boolean isRtl = isLayoutRtl();
super.screenScrolled(screenCenter);
// 循环处理每页
for (int i = 0; i < getChildCount(); i++) {
View v = getPageAt(i);
if (v != null) {
// 获取滑动的进度
float scrollProgress = getScrollProgress(screenCenter, v, i);
float interpolatedProgress;
float translationX;
float maxScrollProgress = Math.max(0, scrollProgress);
float minScrollProgress = Math.min(0, scrollProgress);
if (isRtl) {
translationX = maxScrollProgress * v.getMeasuredWidth();
interpolatedProgress = mZInterpolator.getInterpolation(Math.abs(maxScrollProgress));
} else {
//子页X轴偏移
translationX = minScrollProgress * v.getMeasuredWidth();
//插值器,值是一条向上的抛物线,为了缩放过度更加自然
interpolatedProgress = mZInterpolator.getInterpolation(Math.abs(minScrollProgress));
}
float scale = (1 - interpolatedProgress) +
interpolatedProgress * TRANSITION_SCALE_FACTOR;
float alpha;
if (isRtl && (scrollProgress > 0)) {
alpha = mAlphaInterpolator.getInterpolation(1 - Math.abs(maxScrollProgress));
} else if (!isRtl && (scrollProgress < 0)) {
alpha = mAlphaInterpolator.getInterpolation(1 - Math.abs(scrollProgress));
} else {
// On large screens we need to fade the page as it nears its leftmost position
alpha = mLeftScreenAlphaInterpolator.getInterpolation(1 - scrollProgress);
}
v.setCameraDistance(mDensity * CAMERA_DISTANCE);
int pageWidth = v.getMeasuredWidth();
int pageHeight = v.getMeasuredHeight();
if (PERFORM_OVERSCROLL_ROTATION) {
float xPivot = isRtl ? 1f - TRANSITION_PIVOT : TRANSITION_PIVOT;
boolean isOverscrollingFirstPage = isRtl ? scrollProgress > 0 : scrollProgress < 0;
boolean isOverscrollingLastPage = isRtl ? scrollProgress < 0 : scrollProgress > 0;
//在第一页向右滑时
if (i == 0 && isOverscrollingFirstPage) {
// Overscroll to the left
v.setPivotX(xPivot * pageWidth);
v.setRotationY(-TRANSITION_MAX_ROTATION * scrollProgress);
scale = 1.0f;
alpha = 1.0f;
// On the first page, we don't want the page to have any lateral motion
translationX = 0;
//最后一页向左滑时
} else if (i == getChildCount() - 1 && isOverscrollingLastPage) {
// Overscroll to the right
v.setPivotX((1 - xPivot) * pageWidth);
v.setRotationY(-TRANSITION_MAX_ROTATION * scrollProgress);
scale = 1.0f;
alpha = 1.0f;
// On the last page, we don't want the page to have any lateral motion.
translationX = 0;
} else {
v.setPivotY(pageHeight / 2.0f);
v.setPivotX(pageWidth / 2.0f);
v.setRotationY(0f);
}
}
v.setTranslationX(translationX); //X轴偏移
v.setScaleX(scale); //缩放
v.setScaleY(scale);
v.setAlpha(alpha); //透明度
// If the view has 0 alpha, we set it to be invisible so as to prevent
// it from accepting touches
if (alpha == 0) {
v.setVisibility(INVISIBLE);
} else if (v.getVisibility() != VISIBLE) {
v.setVisibility(VISIBLE);
}
}
}
enableHwLayersOnVisiblePages();
}
基本上就是这样,没什么要过多解释的,试一下效果就比较清楚。下面将他们稍微改动下,变成翻转效果。第三方的Launcher有很多翻页效果,大家可以多尝试。看起来挺高端的,其实也是很简单的效果叠加的。还有,现在很多Launcher都是循环滑动的,具体怎样改为循环的,有机会再开一贴。
// In apps customize, we have a scrolling effect which emulates pulling cards off of a stack.
// http://blog.csdn.net/heymi_csdn
@Override
protected void screenScrolled(int screenCenter) {
final boolean isRtl = isLayoutRtl();
super.screenScrolled(screenCenter);
for (int i = 0; i < getChildCount(); i++) {
View v = getPageAt(i);
if (v != null) {
float scrollProgress = getScrollProgress(screenCenter, v, i);
float interpolatedProgress;
float translationX;
float maxScrollProgress = Math.max(0, scrollProgress);
float minScrollProgress = scrollProgress;//Math.min(0, scrollProgress);
if (isRtl) {
translationX = maxScrollProgress * v.getMeasuredWidth();
interpolatedProgress = mZInterpolator.getInterpolation(Math.abs(maxScrollProgress));
} else {
translationX = minScrollProgress * v.getMeasuredWidth();
interpolatedProgress = mZInterpolator.getInterpolation(Math.abs(minScrollProgress));
}
float scale = (1 - interpolatedProgress) +
interpolatedProgress * TRANSITION_SCALE_FACTOR;
float alpha;
alpha = mAlphaInterpolator.getInterpolation(1 - Math.abs(scrollProgress));
v.setCameraDistance(mDensity * CAMERA_DISTANCE);
int pageWidth = v.getMeasuredWidth();
int pageHeight = v.getMeasuredHeight();
if (PERFORM_OVERSCROLL_ROTATION) {
float xPivot = isRtl ? 1f - TRANSITION_PIVOT : TRANSITION_PIVOT;
boolean isOverscrollingFirstPage = isRtl ? scrollProgress > 0 : scrollProgress < 0;
boolean isOverscrollingLastPage = isRtl ? scrollProgress < 0 : scrollProgress > 0;
if (i == 0 && isOverscrollingFirstPage) {
// Overscroll to the left
v.setPivotX(xPivot * pageWidth); //设置中心点
v.setRotationY(-TRANSITION_MAX_ROTATION * scrollProgress); //Y轴旋转
// On the first page, we don't want the page to have any lateral motion
translationX = 0;
} else if (i == getChildCount() - 1 && isOverscrollingLastPage) {
// Overscroll to the right
v.setPivotX((1 - xPivot) * pageWidth);
v.setRotationY(-TRANSITION_MAX_ROTATION * scrollProgress);
// On the last page, we don't want the page to have any lateral motion.
translationX = 0;
} else {
v.setPivotY(pageHeight / 2.0f);
v.setPivotX(pageWidth / 2.0f);
v.setTranslationX(translationX);//X轴偏移方向
v.setRotationY(180 * scrollProgress);
v.setAlpha(alpha);
}
}
// If the view has 0 alpha, we set it to be invisible so as to prevent
// it from accepting touches
if (alpha == 0 || Math.abs(translationX) > pageWidth/2) {
v.setVisibility(INVISIBLE);
} else if (v.getVisibility() != VISIBLE) {
v.setVisibility(VISIBLE);
}
}
}
enableHwLayersOnVisiblePages();
}
注:本文基于的是android4.4 MTK定制过的代码,和谷歌原生有一点差异。下次有空做一个demo。