前言
本篇是基于AnimationListView框架的,这个框架在上一篇中详细的讲解了,建议阅读本篇前先熟悉 《Android魔术系列:一步步实现对折页面》。
在上一章中我们实现对折的效果同时实现了一个AnimationListView的框架,在这个框架下我们可以实现很多效果。
本篇文章我们就在这个框架下实现一个百叶窗的效果,效果如下:
实现AnimationViewInterface接口
如果想在AnimationListView中应用一种效果,那么就需要实现AnimationViewInterface接口,如下
public class BlindsView extends LinearLayout implements AnimationViewInterface{ 复制代码
BlindsView的具体实现我们稍后在讲解,先看看BlindsView继承LinearLayout,为什么呢?
解析动画组成
我们来看其中一帧的画面,如下
可以看到整个百叶窗效果其实是由一个个小的方形组成的,这些方块做水平翻转的动作,并且在不同列有一个效果的时差,就形成了百叶窗的效果。
所以我们BlindsView实际上包含许多这样的子view,真正的动画是这些子view翻转产生的,所以BlindsView要继承LinearLayout来实现这种宫格布局。
翻转单元——RotateView
上面提到的子view,我们定义为RotateView,继承ImageView以便来装载图片。如下:
public class RotateView extends ImageView { 复制代码
前景背景图
观察下图中指示位置的方块,并对比上一张同一位置的方块。
可以发现当翻转过180度的时候,该方块显示了另外一张图片,实际上是下一页该位置的部分。所以每个RotateView需要前景和背景两张图片,代码如下:
public void setBitmap(Bitmap frontBitmap, Bitmap backBitmap){ if(frontBitmap == null){ return; } mFrontBitmap = frontBitmap; mBackBitmap = backBitmap; mRotatedBackBitmap = null; setImageBitmap(frontBitmap); setScaleType(ScaleType.FIT_XY); //初始化翻转角度 setRotationX(0); setRotationY(0); } 复制代码
代码比较简单,默认显示前景图。
其中有个mRotateBackBitmap
,我们后面会讲。
实现翻转
代码如下:
public void setRotation(float value, boolean isRotateX){ //设置翻转角度 if(isRotateX){ setRotationX(value); } else { setRotationY(value); } //将角度转换为0-360之间,以便后面判断 float rotate = value % 360; if (rotate < 0) { rotate += 360; } /** * 设置缩放:当向垂直翻转时缩小,反之恢复 * 缩放的主要原因是在翻转时,图像会变形为梯形,这时图片中心轴保持原来的宽度, * 则向上翻转那边会变大,部分图像会超出无法显示。所以这里用缩放处理一下, * 至于缩放大小,根据实际需求改变。 */ float scale = rotate > 180 ? Math.abs(rotate - 270) : Math.abs(rotate - 90); scale = scale / 90 * (1 - mScaleMin) + mScaleMin; if(isRotateX){ setScaleX(scale); } else{ setScaleY(scale); } //根据翻转的位置,设置前景/背景图片 if(mBackBitmap != null) { if(mRotatedBackBitmap == null || this.isRotateX != isRotateX) { /** * 首先会根据翻转的方向,对背景图片进行一次翻转 * 这样当翻转时背景图片不会左右上下颠倒 */ Matrix matrix = new Matrix(); if (isRotateX) { matrix.postScale(1, -1); } else { matrix.postScale(-1, 1); } mRotatedBackBitmap = Bitmap.createBitmap(mBackBitmap, 0, 0, mBackBitmap.getWidth(), mBackBitmap.getHeight(), matrix, true); } /** * 当翻转在2、3象限显示背景图,在1、4象限显示前景图 */ if (rotate > 90 && rotate < 270) { setImageBitmap(mRotatedBackBitmap); } else { setImageBitmap(mFrontBitmap); } } this.isRotateX = isRotateX; } 复制代码
两个参数,第一个参数是翻转的角度,第二个参数是翻转的方向(水平还是垂直)。
翻转很简单,调用setRotationX
或setRotationY
函数即可,主要是前景图和背景图的切换。
注意第二部分代码,这里做了缩放的处理,是因为翻转时由于实现了近大远小的效果,所以翻转时处于外侧的一边会增大并超出区域,这样视觉上效果不好,所以做了缩放处理,保证整个翻转过程可以完整的呈现在区域内。大家可以试着将这部分代码去掉对比一下效果,这里就不展示了。
最后一步代码则是根据反转的角度不同设置不同的图片。重点关注背景图,由于背景图实际上应该是水平镜像的,所以使用要提前水平翻转一下,翻转后的就是mRotateBackBitmap
。为了防止每次都做一次翻转操作,判断如果已经有mRotateBackBitmap
并且翻转方向未变则不必再执行。所以如果改变了背景图,要重置mRotateBackBitmap
为null
,就是上面setBitmap
函数提到的。
这样当我们调用setRotate
方法设置不同的角度就能得到不同的翻转效果。
实现翻转动画
对于RotateView其实只需要setRotate
函数,动画部分在BlindsView中处理并调用setRotate
即可。但是我们也希望这个类可以单独使用,所以我加入了它自身的动画处理,如下:
public void rotateXAnimation(float fromRotate, float toRotate, long duration){ rotateAnimation(fromRotate, toRotate, duration, 0, true); } /** * 翻转动画 * @param fromRotate 开始角度 * @param toRotate 结束角度 * @param duration * @param delay 动画延时 * @param isRotateX 是否以X为轴 */ private void rotateAnimation(float fromRotate, float toRotate, long duration, long delay, boolean isRotateX){ if(mAnimator != null){ mAnimator.cancel(); } mAnimator = ValueAnimator.ofFloat(fromRotate, toRotate); mAnimator.setStartDelay(delay); mAnimator.setDuration(duration); mAnimator.start(); mAnimator.addUpdateListener(new RotateListener(isRotateX)); } class RotateListener implements ValueAnimator.AnimatorUpdateListener{ private boolean isRotateX; public RotateListener(boolean isRotateX){ this.isRotateX = isRotateX; } @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (Float)(animation.getAnimatedValue()); setRotation(value, isRotateX); } } 复制代码
其实也很简单,用属性动画来实现即可。这里直接用ValueAnimator,这样动画的值会从fromRotate
逐渐改变至toRotate
。为动画设置一个监听器,并调用setRotate
函数就实现了翻转的动画。
百叶窗——BlindsView
上面我们完成了翻转单元——RotateView,下面讲解如何用这些单元来组成百叶窗的效果。
初始化图片矩阵
将整个的前景和背景图片切割成小图片设置给RotateView,并将这些RotateView以矩阵形式布局到BlindsView中,代码如下:
public void setBitmap(Bitmap frontBitmap, Bitmap backBitmap){ //处理图片 List<Bitmap> subFrontBitmaps = getSubBitmaps(mRowCount, mColumnCount, frontBitmap); List<Bitmap> subBackBitmaps = getSubBitmaps(mRowCount, mColumnCount, backBitmap); setBitmaps(mRowCount, mColumnCount, subFrontBitmaps, subBackBitmaps); } /** * 获取图片阵列 * 将大图片分割为rowCount*columnCount阵列的小图片 * @param rowCount * @param columnCount * @param bitmap * @return */ private List<Bitmap> getSubBitmaps(int rowCount, int columnCount, Bitmap bitmap){ List<Bitmap> subBitmaps = new ArrayList<Bitmap>(); int subWidth = bitmap.getWidth() / columnCount; int subHeight = bitmap.getHeight() / rowCount; for(int i = 0; i < rowCount; i++){ for(int j = 0; j < columnCount; j++){ /** * 这里计算每个叶面图片的大小 * 由于有余数,所以最后一张图片大小单独计算 */ int height = i == rowCount - 1 ? bitmap.getHeight() - subHeight * i : subHeight; int width = j == columnCount - 1 ? bitmap.getWidth() - subWidth * j : subWidth; Bitmap subBitmap = Bitmap.createBitmap(bitmap, subWidth * j, subHeight * i, width, height); subBitmaps.add(subBitmap); } } return subBitmaps; } /** * 设置图片阵列 * 将前景和背景图片的阵列放入每个rotateview中 * @param rowCount * @param columnCount * @param mFrontBitmaps * @param mBackBitmaps */ private void setBitmaps(int rowCount, int columnCount, List<Bitmap> mFrontBitmaps, List<Bitmap> mBackBitmaps){ /** * 为了复用,需要做些处理 * 首先判断现有行/列是否多余,多余直接remove,不足补充 */ //最大行数,是取现有行数和目标行数的最大值。 int maxRow = Math.max(getChildCount() , rowCount); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, 1); params.weight = 1; for(int i = 0; i < maxRow; i++){ LinearLayout subView = null; if(i >= getChildCount() && i < rowCount){ //如果现有行数不足,则补充。每一行都是水平的linearlayout subView = new LinearLayout(getContext()); subView.setOrientation(HORIZONTAL); addView(subView, params); } else if(i < getChildCount() && i >= rowCount){ //如果现有行数过多,则移除 removeViewAt(i); i--; maxRow--; } else{ subView = (LinearLayout)getChildAt(i); } //开始处理每一行中的每项 if(subView != null){ //最大列数,是取现有列数和目标列数的最大值。 int maxColumn = Math.max(subView.getChildCount() , columnCount); LinearLayout.LayoutParams subParams = new LinearLayout.LayoutParams( 1, LinearLayout.LayoutParams.MATCH_PARENT); subParams.weight = 1; for(int j = 0; j < maxColumn; j++){ RotateView rotateView = null; if(j >= columnCount && j < subView.getChildCount()){ //如果现有列过多,则移除 subView.removeViewAt(j); j--; maxColumn--; } else if(j < columnCount && j >= subView.getChildCount()){ //如果现有列不足,则补充。每个叶面是RotateView rotateView = new RotateView(getContext()); subView.addView(rotateView, subParams); } else{ rotateView = (RotateView)subView.getChildAt(j); } //为重新整理好的矩阵填充图片 if(rotateView != null){ int index = i * columnCount + j; rotateView.setBitmap(mFrontBitmaps.get(index), mBackBitmaps.get(index)); rotateView.setScaleMin(0.5f); } } } } } 复制代码
可以看到先调用getSubBitmaps
函数分别将前景和背景图切割并返回一个list。
然后调用setBitmaps
函数,根据指定的行和列循环新建RotateView,传入对应的图片并添加到布局中。
注意,这里复用之前已经存在的RotateView,如果不足则补充,多余的remove掉。
这部分虽然代码较多,但是实际上就是每行add一个水平的LinearLayout(BlindsView本身是垂直的),然后逐行一个个add或复用RotateView并为其setBitmap
。
手动翻转百叶窗
与上一篇对折效果一样,整个百叶窗效果的移动包括手动和自动两个部分。当用户touch屏幕并移动时,百叶窗跟随touch的move事件去移动;当用户touch up或end时,会通过一个animation自动完成剩余的部分。
手动移动阶段的需要实现AnimationViewInterface的setAnimationPrecent方法,如下:
@Override public void setAnimationPercent(float percent, MotionEvent event, boolean isVertical){ mAnimationPercent = percent; //获取总的转动的角度 float value = mAnimationPercent * getTotalVaule(isVertical); /** * 遍历每一个小叶面设置当前的角度 * 根据转动的方向不同,从不同的位置开始翻转 */ for(int i = 0; i < mRowCount; i++){ LinearLayout parent = (LinearLayout)getChildAt(i); for(int j = 0; j < mColumnCount; j++){ RotateView view = (RotateView)parent.getChildAt(j); float subValue; if(value > 0){ if(isVertical){ //向下滑动。从第一行开始转动,每行转动角度依次递减 subValue = value - mSpace * i; } else{ //向右滑动。从第一列开始转动,每列转动角度依次递减 subValue = value - mSpace * j; } //保证转动角度在0到180度内 if(subValue < 0){ subValue = 0; } else if(subValue > 180){ subValue = 180; } } else{ if(isVertical){ //向下滑动。从最后一行开始转动,每行转动角度依次递减(注意由于value是负数,所以数值上是递增) subValue = value + mSpace * (mRowCount - i - 1); } else{ //向左滑动。从最后一列开始转动,每列转动角度依次递减(注意由于value是负数,所以数值上是递增) subValue = value + mSpace * (mColumnCount - j - 1); } //保证转动角度在0到-180度内 if(subValue < -180){ subValue = -180; } else if(subValue > 0){ subValue = 0; } } //注意,如果是上下翻动,角度需要转为负值,否则转动的方向有误 view.setRotation(isVertical ? -subValue : subValue, isVertical); } } } 复制代码
可以看到,一开始我们就通过getTotalValue
计算出一个总的转动角度,这个函数代码如下:
private float getTotalVaule(boolean isVertical){ if(isVertical) { return mSpace * (mRowCount - 1) + 180; } else{ return mSpace * (mColumnCount - 1) + 180; } } 复制代码
这块需要解释一下。从上面的图片可以看到,每一列旋转的角度时不同的,相邻列会差一个角度,就是mSpace
。
那么getTotalValue
函数计算的是一个什么值?
在一个完整翻转过程中,当第一列翻转完成,其他列还没有,所以过程并未结束。
这时假设第一列继续翻转,当第二列翻转完成,第一列已经翻转了mSpace * 1 + 180
。那么继续直到最后一列也完全翻转过来,那么第一列实际翻转了mSpace * (columnCount - 1) + 180
。
所以mAnimationPercent * getTotalVaule(isVertical)
实际上就是第一列当前的翻转角度了,这样就可以计算出其他列的翻转角度。为每个RotateView设置rotation
即可。
但是注意这并不是真正的翻转角度,当已经完全翻转180度后就不再需要翻转。
代码中处理了四个方向的翻转,所以计算上多少有些不同,思路是一样的。
自动翻转百叶窗
自动阶段通过实现startAnimation
函数,代码如下:
@Override public void startAnimation(boolean isVertical, MotionEvent event, float toPercent){ if(mAnimator != null && mAnimator.isRunning()){ return; } mAnimator = ValueAnimator.ofFloat(mAnimationPercent, toPercent); //动画持续时间根据起始位置不同 mAnimator.setDuration((long) (Math.abs(toPercent - mAnimationPercent) * mDuration)); mAnimator.start(); OnAnimationListener onAnimationListener = new OnAnimationListener(isVertical, toPercent); mAnimator.addUpdateListener(onAnimationListener); mAnimator.addListener(onAnimationListener); } class OnAnimationListener implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener{ private boolean isVertical; private float toPercent; public OnAnimationListener(boolean isVertical, float toPercent){ this.isVertical = isVertical; this.toPercent = toPercent; } @Override public void onAnimationUpdate(ValueAnimator animation) { setAnimationPercent((float)animation.getAnimatedValue(), null, isVertical); } @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { mAnimationPercent = 0; if(mOnAnimationViewListener == null){ return; } if(toPercent == 1){ mOnAnimationViewListener.pagePrevious(); } else if(toPercent == -1){ mOnAnimationViewListener.pageNext(); } } @Override public void onAnimationCancel(Animator animation) { mAnimationPercent = 0; } @Override public void onAnimationRepeat(Animator animation) { } } 复制代码
通过代码可以看到就是通过监听一个float的属性动画,然后通过setAnimationPrecent
改变翻转状态即可。
注意在动画结束时调用切页的回调。
这部分与上一篇对折效果类似,就不细说了。
总结一下
通过这两篇文章,大家应该对AnimationListView这个框架有了了解。通过这个框架我们还可以实现更多更酷的效果,代码大体上可以参考这两个效果。关于这个框架及实现我们暂时告一段落,接下来会分析一些其他的,以后有机会我们可以在这个框架上实现更多的效果,大家如果有什么好的想法或自己实现的效果可以留言。