1、效果展示
2、实现AnimationViewInterface接口
public class BlindsView extends LinearLayout implements AnimationViewInterface{
3、解析动画组成
![](https://i-blog.csdnimg.cn/blog_migrate/72aec241257d8cef4f129fa8214b53f2.jpeg)
4、翻转单元——RotateView
public class RotateView extends ImageView {
1)前景背景图
![](https://i-blog.csdnimg.cn/blog_migrate/f46a89baf8b9201a83484f9e17d1c01c.jpeg)
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,我们后面会讲。
2)实现翻转
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方法设置不同的角度就能得到不同的翻转效果。
3)实现翻转动画
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函数就实现了翻转的动画。
5、百叶窗——BlindsView
1)初始化图片矩阵
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。
2)手动翻转百叶窗
@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度后就不再需要翻转。
代码中处理了四个方向的翻转,所以计算上多少有些不同,思路是一样的。
3)自动翻转百叶窗
@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改变翻转状态即可。
注意在动画结束时调用切页的回调。
这部分与上一篇对折效果类似,就不细说了。
6、总结一下
源码:
![](https://i-blog.csdnimg.cn/blog_migrate/704f327570e432d55493ed455a8e432a.jpeg)