Android魔法(第四弹)—— 一步步实现百叶窗效果

原创 2017年09月13日 17:37:12
本篇是基于AnimationListView框架的,这个框架在上一篇中详细的讲解了,建议阅读本篇前先熟悉一下。
Android魔法系列:
http://blog.csdn.net/chzphoenix/article/details/77962259

项目的github地址:FastWidget4Android  很多炫酷的自定义效果,欢迎fork和star!


1、效果展示

在上一章中我们实现对折的效果同时实现了一个AnimationListView的框架,在这个框架下我们可以实现很多效果。
本篇文章我们就在这个框架下实现一个百叶窗的效果,效果如下:

2、实现AnimationViewInterface接口

如果想在AnimationListView中应用一种效果,那么就需要实现AnimationViewInterface接口,如下
public class BlindsView extends LinearLayout implements AnimationViewInterface{

BlindsView的具体实现我们稍后在讲解,先看看BlindsView继承LinearLayout,为什么呢?

3、解析动画组成

我们来看其中一帧的画面,如下

可以看到整个百叶窗效果其实是由一个个小的方形组成的,这些方块做水平翻转的动作,并且在不同列有一个效果的时差,就形成了百叶窗的效果。
所以我们BlindsView实际上包含许多这样的子view,真正的动画是这些子view翻转产生的,所以BlindsView要继承LinearLayout来实现这种宫格布局。

4、翻转单元——RotateView

上面提到的子view,我们定义为RotateView,继承ImageView以便来装载图片。如下:
public class RotateView extends ImageView {

1)前景背景图

观察下图中指示位置的方块,并对比上一张同一位置的方块。

可以发现当翻转过180度的时候,该方块显示了另外一张图片,实际上是下一页该位置的部分。所以每个RotateView需要前景和背景两张图片,代码如下:
public void setBitmap(Bitmap frontBitmapBitmap 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 * (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(-11);
            }
            mRotatedBackBitmap = Bitmap.createBitmap(mBackBitmap00,
                    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)实现翻转动画

对于RotateView其实只需要setRotate函数,动画部分在BlindsView中处理并调用setRotate即可。但是我们也希望这个类可以单独使用,所以我加入了它自身的动画处理,如下:
public void rotateXAnimation(float fromRotate, float toRotate, long duration){
    rotateAnimation(fromRotatetoRotateduration0, 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(fromRotatetoRotate);
    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(valueisRotateX);
    }
}
其实也很简单,用属性动画来实现即可。这里直接用ValueAnimator,这样动画的值会从fromRotate逐渐改变至toRotate。为动画设置一个监听器,并调用setRotate函数就实现了翻转的动画。

5、百叶窗——BlindsView

上面我们完成了翻转单元——RotateView,下面讲解如何用这些单元来组成百叶窗的效果。

1)初始化图片矩阵

将整个的前景和背景图片切割成小图片设置给RotateView,并将这些RotateView以矩阵形式布局到BlindsView中,代码如下:
public void setBitmap(Bitmap frontBitmapBitmap backBitmap){
    //处理图片
    List<Bitmap> subFrontBitmaps = getSubBitmaps(mRowCountmColumnCountfrontBitmap);
    List<Bitmap> subBackBitmaps = getSubBitmaps(mRowCountmColumnCountbackBitmap);
    setBitmaps(mRowCountmColumnCountsubFrontBitmapssubBackBitmaps);
}

/**
 * 获取图片阵列
 * 将大图片分割为rowCount*columnCount阵列的小图片
 * @param rowCount
 @param columnCount
 @param bitmap
 @return
 */
private List<Bitmap> getSubBitmaps(int rowCount, int columnCountBitmap bitmap){
    List<Bitmap> subBitmaps = new ArrayList<Bitmap>();
    int subWidth = bitmap.getWidth() / columnCount;
    int subHeight = bitmap.getHeight() / rowCount;
    for(int i = 0i < rowCounti++){
        for(int j = 0j < columnCountj++){
            /**
             * 这里计算每个叶面图片的大小
             * 由于有余数,所以最后一张图片大小单独计算
             */
            int height = i == rowCount - ? bitmap.getHeight() - subHeight * i : subHeight;
            int width = j == columnCount - ? bitmap.getWidth() - subWidth * j : subWidth;
            Bitmap subBitmap = Bitmap.createBitmap(bitmapsubWidth * jsubHeight * iwidthheight);
            subBitmaps.add(subBitmap);
        }
    }
    return subBitmaps;
}

/**
 * 设置图片阵列
 * 将前景和背景图片的阵列放入每个rotateview中
 * @param rowCount
 @param columnCount
 @param mFrontBitmaps
 @param mBackBitmaps
 */
private void setBitmaps(int rowCount, int columnCountList<Bitmap> mFrontBitmapsList<Bitmap> mBackBitmaps){
    /**
     * 为了复用,需要做些处理
     * 首先判断现有行/列是否多余,多余直接remove,不足补充
     */
    //最大行数,是取现有行数和目标行数的最大值。
    int maxRow = Math.max(getChildCount() rowCount);
    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
            LinearLayout.LayoutParams.MATCH_PARENT1);
    params.weight 1;
    for(int i = 0i < maxRowi++){
        LinearLayout subView = null;
        if(i >= getChildCount() && i < rowCount){
            //如果现有行数不足,则补充。每一行都是水平的linearlayout
            subView = new LinearLayout(getContext());
            subView.setOrientation(HORIZONTAL);
            addView(subViewparams);
        }
        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(
                    1LinearLayout.LayoutParams.MATCH_PARENT);
            subParams.weight 1;
            for(int j = 0j < maxColumnj++){
                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(rotateViewsubParams);
                }
                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)手动翻转百叶窗

与上一篇对折效果一样,整个百叶窗效果的移动包括手动和自动两个部分。当用户touch屏幕并移动时,百叶窗跟随touch的move事件去移动;当用户touch up或end时,会通过一个animation自动完成剩余的部分。
手动移动阶段的需要实现AnimationViewInterface的setAnimationPrecent方法,如下:
@Override
public void setAnimationPercent(float percentMotionEvent event, boolean isVertical){
    mAnimationPercent = percent;
    //获取总的转动的角度
    float value = mAnimationPercent * getTotalVaule(isVertical);
    /**
     * 遍历每一个小叶面设置当前的角度
     * 根据转动的方向不同,从不同的位置开始翻转
     */
    for(int i = 0i < mRowCounti++){
        LinearLayout parent = (LinearLayout)getChildAt(i);
        for(int j = 0j < mColumnCountj++){
            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 : subValueisVertical);
        }
    }
}

可以看到,一开始我们就通过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)自动翻转百叶窗

自动阶段通过实现startAnimation函数,代码如下:
@Override
public void startAnimation(boolean isVerticalMotionEvent event, float toPercent){
    if(mAnimator != null && mAnimator.isRunning()){
        return;
    }
    mAnimator = ValueAnimator.ofFloat(mAnimationPercenttoPercent);
    //动画持续时间根据起始位置不同
    mAnimator.setDuration((long) (Math.abs(toPercent - mAnimationPercent) * mDuration));
    mAnimator.start();
    OnAnimationListener onAnimationListener = new OnAnimationListener(isVerticaltoPercent);
    mAnimator.addUpdateListener(onAnimationListener);
    mAnimator.addListener(onAnimationListener);
}

class OnAnimationListener implements ValueAnimator.AnimatorUpdateListenerAnimator.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、总结一下

通过这两篇文章,大家应该对AnimationListView这个框架有了了解。通过这个框架我们还可以实现更多更酷的效果,代码大体上可以参考这两个效果。关于这个框架及实现我们暂时告一段落,接下来会分析一些其他的,以后有机会我们可以在这个框架上实现更多的效果,大家如果有什么好的想法或自己实现的效果可以留言。

项目的github地址:

FastWidget4Android  很多炫酷的自定义效果,欢迎fork和star!

Android魔法系列:
http://blog.csdn.net/chzphoenix/article/details/77962259

联系我




版权声明:本文为博主原创文章,转载请注明出处:http://blog.csdn.net/chzphoenix。 举报

相关文章推荐

Android DataBinding使用总结(一)

MVVM-DataBinding使用总结(一)前言2015年谷歌I/O大会上介绍了一个框架DataBinding,DataBinding是一个数据绑定框架,以前我们在Activity里写很多的find...

Android官方数据绑定框架DataBinding(一)

还记得在博客《高逼格UI-ASD(Android Support Design)》的开始曾经说过, Android最新推出了一个官方的数据绑定框架-Data Binding Library。现在gi...

我是如何成为一名python大咖的?

人生苦短,都说必须python,那么我分享下我是如何从小白成为Python资深开发者的吧。2014年我大学刚毕业..

DataBinding

介绍的是DataBinding这一部分,该部分内容将会作为MVVM架构中View和ViewModel之间的桥梁,用于连接这两个模块,所以在学习MVVM之前把DataBinding学会了是非常有必要的。...

DataBinding使用教程(四):BaseObservable与双向绑定

引言这篇文章主要讲两个知识点: 双向绑定:主要是把上一篇文章中的一些注解通过具体实例再详细说一下,通过这些实例,尽量把双向绑定说的清楚一些。 BaseObservable:包括与BaseObserva...

Data Binding Guide——google官方文档翻译(上)

android引入MVVM框架时间还不长,目前还很少有应用到app中的。但它是比较新的技术,使用它来搭建项目能省很多代码,而且能使用代码架构比较清晰。本篇文章是我在学习MVVM时翻译的,篇幅比较长,先...

DataBinding的基本使用(三)

DataBinding的基本使用(三)DataBinding基本使用包括以下内容: - 单纯的摆脱findviewbyid - 绑定基本数据类型及String - 绑定Model数据 - 绑定...

如何更高效的使用MVP以及官方MVP架构解析

转载请标明出处: http://blog.csdn.net/dantestones/article/details/51445208 Android mvp 架构的自述中我简单的介绍了mvp,...

程序员的八重境界

看到一篇有趣的文章The Eight Levels of Programmers。以前似乎看过不少这种程序员的多少个级别、境界,但这篇语言很风趣,而且分类比较细化,让人觉得挺合情合理、无法反驳的。绝大...
  • dc_726
  • dc_726
  • 2017-08-31 04:58
  • 22444

DataBinding的基本使用(一)

DataBinding的基本使用(一)1.DataBinding介绍2015年谷歌I/O大会上介绍了一个框架DataBinding,DataBinding是一个数据绑定框架,以前我们在Activity...

DataBinding使用进阶

android原生支持MVVM-DataBinding从2015年开始Android官方支持MVVM框架 首先我们需要知道MVVM是什么? MVVM是从一种开发模式,不同于MVC和MVP,MVVM...
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)