android动效_android动画效果 - CSDN
精华内容
参与话题
  • Android使用SVG矢量图打造酷炫动效

    万次阅读 多人点赞 2017-01-13 14:39:30
    这个效果我们需要考虑以下几个问题: 1. 这是图片还是文字; 2. 如果是图片该如何拿到图形的边沿线坐标,如果是文字呢? 3. 如果拿到了边沿线坐标,如何让光线沿着路径跑动; 4. 怎么处理过程的衔接;...


          尊重原创,欢迎转载,转载请注明: FROM  GA_studio   http://blog.csdn.net/tianjian4592


            一个真正酷炫的动效往往让人虎躯一震,话不多说,咱们先瞅瞅效果:




    --------------------------------------------------

    如果你想看 GAStudio Github主页,请戳这里; 
    如果你想看 GAStudio更多技术文章,请戳这里; 
    QQ技术交流群:277582728; 

    --------------------------------------------------


    这个效果我们需要考虑以下几个问题:

    1. 这是图片还是文字;

    2. 如果是图片该如何拿到图形的边沿线坐标,如果是文字呢?

    3. 如果拿到了边沿线坐标,如何让光线沿着路径跑动;

    4. 怎么处理过程的衔接;


    以上四个问题似乎不是太好处理,而这几个问题也正好是这个效果精华所在,接下来咱们一个一个进行考虑,当然这种考虑已经基于一些国外大神的基础之上;

    首先这是图片还是文字?

    答案是:背景是图片,表面的文字还是图片,有些同学可能会说了,靠,这么没含量,一个帧动画而已,还虎躯一震,XXXXX,当然,答案肯定不会是这样的,背景我就不说了,普通的jpg或png图,但文字则是SVG格式的矢量图;

    有了第一个问题的答案,我们来看第二个问题,如何拿到文字图形的边沿坐标;

    要回答这个问题,我们先来简单的了解一个SVG(矢量图);

    SVG 意为可缩放矢量图形(Scalable Vector Graphics),是使用 XML 来描述二维图形和绘图程序的语言;

    使用 SVG 的优势在于:

    1.SVG 可被非常多的工具读取和修改(比如记事本),由于使用xml格式定义,所以可以直接被当作文本文件打开,看里面的数据;

    2.SVG 与 JPEG 和 GIF 图像比起来,尺寸更小,且可压缩性更强,SVG 图就相当于保存了关键的数据点,比如要显示一个圆,需要知道圆心和半径,那么SVG 就只保存圆心坐标和半径数据,而平常我们用的位图都是以像素点的形式根据图片大小保存对应个数的像素点,因而SVG尺寸更小;

    3.SVG 是可伸缩的,平常使用的位图拉伸会发虚,压缩会变形,而SVG格式图片保存数据进行运算展示,不管多大多少,可以不失真显示;

    4.SVG 图像可在任何的分辨率下被高质量地打印;

    5.SVG 可在图像质量不下降的情况下被放大;

    6.SVG 图像中的文本是可选的,同时也是可搜索的(很适合制作地图);

    7.SVG 可以与 Java 技术一起运行;

    8.SVG 是开放的标准;

    9.SVG 文件是纯粹的 XML;


    看起来好厉害的样子,还是回到我们的问题,从SVG图中我们可否拿到我们想要的数据点呢?根据上面的介绍,答案当然是肯定的,从SVG图中我们可以拿到我们想要的所有数据;

    好的,拿到数据之后,怎么让一条线沿着路径跑起来呢?毋庸置疑,我们需要用到path;

    最后我们根据效果的需要,设置几个绘制过程,进行绘制;

    接下来我们一起来解决以上问题:

    既然SVG是公认的xml文件格式定义的,那么我们则可以通过解析xml文件拿到对应SVG图的所有数据,我们先看下 path 类型的SVG 数据:


    <?xml version="1.0" standalone="no"?>
    <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" 
    "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
    
    <svg width="100%" height="100%" version="1.1"
    xmlns="http://www.w3.org/2000/svg">
    
    <path d="M250 150 L150 350 L350 350 Z" />
    
    </svg>


    上面有一个path 标签,里面用到了 M 和 Z 指令,M 就相当于 android Path 里的moveTo(),Z 则相当于 Path 里的close();

    我们先看下SVG 里关于path 有哪些指令:

    M = moveto   相当于 android Path 里的moveTo(),用于移动起始点
    L = lineto   相当于 android Path 里的lineTo(),用于画线
    H = horizontal lineto     用于画水平线
    V = vertical lineto       用于画竖直线
    C = curveto               相当于cubicTo(),三次贝塞尔曲线
    S = smooth curveto        同样三次贝塞尔曲线,更平滑
    Q = quadratic Belzier curve             quadTo(),二次贝塞尔曲线
    T = smooth quadratic Belzier curveto    同样二次贝塞尔曲线,更平滑
    A = elliptical Arc   相当于arcTo(),用于画弧
    Z = closepath     相当于closeTo(),关闭path

    了解了以上path相关的指令,就可以看懂path构成的SVG图的数据了,除此之外,SVG里还定义了一些基本的图形和效果:



    更多介绍和使用大家可以看 W3School


    好,以上内容,我们已经知道 SVG 图是通过 Xml 格式定义的,并且里面用到了一些基本的指令对数据进行组装,构成基本图形或复杂的路径;

    而对于我们来说 ,这个xml 如何拿到呢?

    1.我们根据最后要做的效果,利用PS等作图软件设计制作出想要的图形;




    2. 使用 GIMP 之类的矢量图软件导出图片的SVG数据,方法如下:

    先使用魔棒工具快速建立选区:




    然后将选区导出为path:




    这个时候在软件的右边栏就可以看见生成的路径了,然后将路径导出:




    过以上几步,我们就拿到了我们自己设计的文字或图形SVG图的Path数据,上面图片的SVG信息如下:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
                  "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
    
    <svg xmlns="http://www.w3.org/2000/svg"
         width="6.95746in" height="1.82269in"
         viewBox="0 0 668 175">
      <path id="Selection"
            fill="none" stroke="black" stroke-width="1"
            d="M 530.00,34.00
               C 530.00,34.00 526.08,59.00 526.08,59.00
                 526.08,59.00 518.00,105.00 518.00,105.00
                 518.00,105.00 515.42,119.00 515.42,119.00
                 515.42,119.00 513.26,125.01 513.26,125.01
                 513.26,125.01 506.00,126.00 506.00,126.00
                 506.00,126.00 496.00,126.00 496.00,126.00
                 496.00,126.00 496.00,120.00 496.00,120.00
                 490.87,124.16 486.71,126.42 480.00,126.91
                 475.71,127.22 471.06,126.94 467.00,125.44
                 454.13,120.68 451.86,110.19 452.00,98.00
                 452.22,79.34 465.14,64.55 484.00,63.18
                 492.14,62.59 498.96,65.71 504.00,72.00
                 504.00,72.00 510.00,34.00 510.00,34.00
                 510.00,34.00 530.00,34.00 530.00,34.00 Z
               M 551.00,56.89
               C 539.01,55.86 537.45,39.82 551.00,35.55
                 568.60,33.45 567.67,58.33 551.00,56.89 Z 
    
     中间段省略

     M 263.00,134.00
               C 263.00,134.00 263.00,145.00 263.00,145.00
                 263.00,145.00 202.00,145.00 202.00,145.00
                 202.00,145.00 202.00,134.00 202.00,134.00
                 202.00,134.00 263.00,134.00 263.00,134.00 Z" />
    </svg>
    


    根据图形路径的复杂度,生成的path数据复杂度也不一样,但格式也算是非常的清楚,即采用一定的指令把数据点进行拼接;

    现在有了这些数据点,我们需要做的则是对数据进行解析,封装成我们要的Path;

    解析的过程也无非是 遇到指令则采用android Path 里的对应方法进行置换,解析方式如下:

    public Path parsePath(String s) throws ParseException {
            mCurrentPoint.set(Float.NaN, Float.NaN);
            mPathString = s;
            mIndex = 0;
            mLength = mPathString.length();
    
            PointF tempPoint1 = new PointF();
            PointF tempPoint2 = new PointF();
            PointF tempPoint3 = new PointF();
    
            Path p = new Path();
            p.setFillType(Path.FillType.WINDING);
    
            boolean firstMove = true;
            while (mIndex < mLength) {
                char command = consumeCommand();
                boolean relative = (mCurrentToken == TOKEN_RELATIVE_COMMAND);
                switch (command) {
                    case 'M':
                    case 'm': {
                        // m指令,相当于android 里的 moveTo()
                        boolean firstPoint = true;
                        while (advanceToNextToken() == TOKEN_VALUE) {
                            consumeAndTransformPoint(tempPoint1,
                                    relative && mCurrentPoint.x != Float.NaN);
                            if (firstPoint) {
                                p.moveTo(tempPoint1.x, tempPoint1.y);
                                firstPoint = false;
                                if (firstMove) {
                                    mCurrentPoint.set(tempPoint1);
                                    firstMove = false;
                                }
                            } else {
                                p.lineTo(tempPoint1.x, tempPoint1.y);
                            }
                        }
                        mCurrentPoint.set(tempPoint1);
                        break;
                    }
    
                    case 'C':
                    case 'c': {
                        // c指令,相当于android 里的 cubicTo()
                        if (mCurrentPoint.x == Float.NaN) {
                            throw new ParseException("Relative commands require current point", mIndex);
                        }
    
                        while (advanceToNextToken() == TOKEN_VALUE) {
                            consumeAndTransformPoint(tempPoint1, relative);
                            consumeAndTransformPoint(tempPoint2, relative);
                            consumeAndTransformPoint(tempPoint3, relative);
                            p.cubicTo(tempPoint1.x, tempPoint1.y, tempPoint2.x, tempPoint2.y,
                                    tempPoint3.x, tempPoint3.y);
                        }
                        mCurrentPoint.set(tempPoint3);
                        break;
                    }
    
                    case 'L':
                    case 'l': {
                        // 相当于lineTo()进行画直线
                        if (mCurrentPoint.x == Float.NaN) {
                            throw new ParseException("Relative commands require current point", mIndex);
                        }
    
                        while (advanceToNextToken() == TOKEN_VALUE) {
                            consumeAndTransformPoint(tempPoint1, relative);
                            p.lineTo(tempPoint1.x, tempPoint1.y);
                        }
                        mCurrentPoint.set(tempPoint1);
                        break;
                    }
    
                    case 'H':
                    case 'h': {
                        // 画水平直线
                        if (mCurrentPoint.x == Float.NaN) {
                            throw new ParseException("Relative commands require current point", mIndex);
                        }
    
                        while (advanceToNextToken() == TOKEN_VALUE) {
                            float x = transformX(consumeValue());
                            if (relative) {
                                x += mCurrentPoint.x;
                            }
                            p.lineTo(x, mCurrentPoint.y);
                        }
                        mCurrentPoint.set(tempPoint1);
                        break;
                    }
    
                    case 'V':
                    case 'v': {
                        // 画竖直直线
                        if (mCurrentPoint.x == Float.NaN) {
                            throw new ParseException("Relative commands require current point", mIndex);
                        }
    
                        while (advanceToNextToken() == TOKEN_VALUE) {
                            float y = transformY(consumeValue());
                            if (relative) {
                                y += mCurrentPoint.y;
                            }
                            p.lineTo(mCurrentPoint.x, y);
                        }
                        mCurrentPoint.set(tempPoint1);
                        break;
                    }
    
                    case 'Z':
                    case 'z': {
                        // 封闭path
                        p.close();
                        break;
                    }
                }
    
            }
    
            return p;
        }
    

    有了图形对应的path,我们只需要按照我们想要的效果进行绘制即可,具体过程不再细讲,大家看代码:

    @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            if (mState == STATE_NOT_STARTED || mGlyphData == null) {
                return;
            }
    
            long t = System.currentTimeMillis() - mStartTime;
    
            // 绘制出现前的边沿线和跑动过程
            for (int i = 0; i < mGlyphData.length; i++) {
                float phase = MathUtil.constrain(0, 1,
                        (t - (mTraceTime - mTraceTimePerGlyph) * i * 1f / mGlyphData.length)
                                * 1f / mTraceTimePerGlyph);
                float distance = INTERPOLATOR.getInterpolation(phase) * mGlyphData[i].length;
                mGlyphData[i].paint.setColor(mTraceResidueColors[i]);
                mGlyphData[i].paint.setPathEffect(new DashPathEffect(
                        new float[] {
                                distance, mGlyphData[i].length
                        }, 0));
                canvas.drawPath(mGlyphData[i].path, mGlyphData[i].paint);
    
                mGlyphData[i].paint.setColor(mTraceColors[i]);
                mGlyphData[i].paint.setPathEffect(new DashPathEffect(
                        new float[] {
                                0, distance, phase > 0 ? mMarkerLength : 0,
                                mGlyphData[i].length
                        }, 0));
                canvas.drawPath(mGlyphData[i].path, mGlyphData[i].paint);
            }
    
            if (t > mFillStart) {
                if (mState < STATE_FILL_STARTED) {
                    changeState(STATE_FILL_STARTED);
                }
    
                // 绘制渐变出现的过程,即改变alpha过程
                float phase = MathUtil.constrain(0, 1, (t - mFillStart) * 1f / mFillTime);
                for (int i = 0; i < mGlyphData.length; i++) {
                    GlyphData glyphData = mGlyphData[i];
                    mFillPaint.setARGB((int) (phase * ((float) mFillAlphas[i] / (float) 255) * 255),
                            mFillReds[i],
                            mFillGreens[i],
                            mFillBlues[i]);
                    canvas.drawPath(glyphData.path, mFillPaint);
                }
            }
    
            if (t < mFillStart + mFillTime) {
                ViewCompat.postInvalidateOnAnimation(this);
            } else {
                changeState(STATE_FINISHED);
            }
        }

    好了,主要的问题和思路基本如上,有些人可能会说,你这讲的跟UX分享似的,没毛线用,其实我的目的只有一个,那就是不管你是否能看懂代码,都能按照我上面所说做出自己想要的效果,并加以改变,灵活运用,毕竟轮子不需要重复造!


    我本人也是对SVG矢量图刚有所了解,主要参考国外大神的一篇博客,链接如下:http://www.willowtreeapps.com/blog/muzei-esque-animated-svg-drawing-for-android/ 


    CSDN源码下载地址:http://download.csdn.net/detail/tianjian4592/8548495


    --------------------------------------------------

    如果你想看 GAStudio Github主页,请戳这里; 
    如果你想看 GAStudio更多技术文章,请戳这里; 
    QQ技术交流群:277582728; 

    --------------------------------------------------





    展开全文
  •  最近新需求有一个热评列表需要做成轮流从屏幕底部向上弹出的动画效果,看了效果图第一时间就想到了弹幕实现,但是思考一番后又否定了这个想法,首先这个热评列表不像弹幕需要实时获取播放,第二屏幕上展示的评论条数有限,固定最多就几条,再有新的弹出就把最上面一条消失掉,不想弹幕似的满屏都是,第三就是我看了B站的开源弹幕库,并不支持竖向的弹幕播放,如果要通过弹幕的方式实现这个需求的话,还需要自己对这个库进行扩展,有点舍近求远的意思。

    思前想后,我想起了ViewGroup可以设置添加和删除内部View时的动画,那么它是不是可以满足我对功能的所有需求呢?于是网上百度了一下,果然有用这种方式做类似效果的,比如这篇Android UI:上下滚动的评论弹幕

    他人实现效果​

    通过对ViewGroup设置LayoutTransition来实现控制View添加和删除时的动画效果,界面固定显示四条评论,整个评论的轮播都是复用四个TextView,当显示超过四条评论时删除第一条显示的评论。但是我发现它这个Demo只有在添加View的时候执行了提前设置的动画,删除的时候并没有执行。总之先Copy下来跑一下看看吧(复制粘贴Levi能力满级),不跑不知道,一跑吓一跳,连添加View时的动画都不能正常执行,当添加第五个View时(也就是第五条评论,复用的第一条评论内容),新增的VIew是直接显示出来,并没有执行动画,在这之前的四条都是可以正常执行的。

    我开始怀疑是不是因为在新增之前执行了删除操作所以出了问题,然后我就把删除的代码注掉又跑了一遍,果然没问题了,于是我就大胆猜测ViewGroup不能同时执行添加和删除的动画,为了验证我的猜想,我又去百度了(百度能力满分),找了一圈发现有一篇博客也是这个问题,并且和我推断出了相同的结论,因为当时也没有收藏书签,也懒得找了,就不放链接了。

    除了我遇到的问题,在百度的过程中发现LayoutTransition挺多坑的,具体可以看看启舰大神的这篇博客自定义控件三部曲之动画篇(十二)——animateLayoutChanges与LayoutTransition

    解决办法

    既然不能同时执行添加和删除的动画,那么我可以监听添加的动画,等它执行完成后再进行删除操作,但是这样一来,添加和删除的动画就不能同步执行了,这样效果就会大打折扣,怎么办呢,曲线救国,再添加动画开始的时候对对要删除的VIew执行一个透明度变化的动画,等添加动画结束后被删除的VIew也就变透明了,这时再进行一个删除的操作,就可以神不知鬼不觉的实现我们要的效果了。注意,这里删除时也会有删除的动画执行,所以我们需要对删除的动画进行一个处理,以防出现错误的效果。

    上代码

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        android:orientation="vertical">
    
    
        <LinearLayout
            android:id="@+id/ll_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_gravity="bottom"
            android:divider="@drawable/divider"
            android:orientation="vertical"
            android:padding="15dp"
            android:showDividers="middle">
    
        </LinearLayout>
    
    </RelativeLayout>
    public class Main2Activity extends AppCompatActivity {
    
        private LinearLayout llContainer;
        LayoutTransition transition;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            llContainer = (LinearLayout) findViewById(R.id.ll_container);
            transition = new LayoutTransition();
            //添加动画
            ObjectAnimator valueAnimator = ObjectAnimator.ofFloat(null, "alpha", 0, 1);
            valueAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {
                    //当前展示超过四条,执行删除动画
                    if (llContainer.getChildCount() == 4) {
                        handler.sendEmptyMessage(1);
                    }
                }
    
                @Override
                public void onAnimationEnd(Animator animation) {
                    if (llContainer.getChildCount() == 5)
                        //动画执行完毕,删除view
                        handler.sendEmptyMessage(2);
                }
    
                @Override
                public void onAnimationCancel(Animator animation) {
    
                }
    
                @Override
                public void onAnimationRepeat(Animator animation) {
    
                }
            });
            transition.setAnimator(LayoutTransition.APPEARING, valueAnimator);
            //删除动画
            PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0, 0);
            ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(null, new PropertyValuesHolder[]{alpha}).setDuration(transition.getDuration(LayoutTransition.DISAPPEARING));
    
            transition.setAnimator(LayoutTransition.DISAPPEARING, objectAnimator);
            llContainer.setLayoutTransition(transition);
        }
    
        private String[] texts = new String[]{
                "火来我在灰烬中等你",
                "我对这个世界没什么可说的。我对这个世界没什么可说的。我对这个世界没什么可说的。",
                "侠之大者,为国为民。",
                "为往圣而继绝学"};
       
    
        Pools.SimplePool<TextView> textViewPool = new Pools.SimplePool<>(texts.length);
    
        private TextView obtainTextView() {
            TextView textView = textViewPool.acquire();
            if (textView == null) {
                textView = new TextView(Main2Activity.this);
                textView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
                textView.setPadding(dp2px(10), dp2px(5), dp2px(10), dp2px(5));
                textView.setTextColor(0xffffffff);
                textView.setMaxLines(1);
                textView.setEllipsize(TextUtils.TruncateAt.END);
                textView.setGravity(Gravity.CENTER);
                textView.setTextSize(15);
                textView.setTextColor(0xffffffff);
                Drawable drawable = getResources().getDrawable(R.mipmap.circle_head);
                drawable.setBounds(0, 0, 80, 80);
                textView.setCompoundDrawablesRelative(drawable, null, null, null);
                textView.setCompoundDrawablePadding(10);
                switch (index) {
                    case 0:
                        textView.setBackgroundDrawable(ContextCompat.getDrawable(Main2Activity.this, R.drawable.rect_black));
                        break;
                    case 1:
                        textView.setBackgroundDrawable(ContextCompat.getDrawable(Main2Activity.this, R.drawable.rect_blue));
    
                        break;
                    case 2:
                        textView.setBackgroundDrawable(ContextCompat.getDrawable(Main2Activity.this, R.drawable.rect_green));
    
                        break;
                    case 3:
                        textView.setBackgroundDrawable(ContextCompat.getDrawable(Main2Activity.this, R.drawable.rect_red));
    
                        break;
                }
            }
            textView.setText(texts[index]);
            return textView;
        }
    
        private int dp2px(float dp) {
            DisplayMetrics displayMetrics = new DisplayMetrics();
            this.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
            return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, displayMetrics);
        }
    
    
        int index = 0;
        @SuppressLint("HandlerLeak")
        private Handler handler = new Handler() {
            @SuppressLint("ResourceAsColor")
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                switch (msg.what) {
                    case 0:
                        TextView textView = obtainTextView();
                        llContainer.addView(textView);
                        sendEmptyMessageDelayed(0, 2000);
                        index++;
                        if (index == 4) {
                            index = 0;
                        }
                        break;
                    case 1:
                        //给展示的第一个view增加渐变透明动画
                        llContainer.getChildAt(0).animate().alpha(0).setDuration(transition.getDuration(LayoutTransition.APPEARING)).start();
                        break;
                    case 2:
                        //删除顶部view
                        llContainer.removeViewAt(0);
                        break;
                }
            }
        };
    
        @Override
        protected void onResume() {
            super.onResume();
            handler.sendEmptyMessage(0);
        }
    
        @Override
        protected void onPause() {
            super.onPause();
            handler.removeMessages(0);
        }
    }

    上效果图

     

    展开全文
  • 前两天我们这边的头儿给我说,有个 gif 动效很不错,可以考虑用来做项目里的loading,问我能不能实现,看了下效果确实不错,也还比较有新意,复杂度也不是非常高,所以就花时间给做了,我们先一起看下原gif图效果: ...

    前两天我们这边的头儿给我说,有个 gif 动效很不错,可以考虑用来做项目里的loading,问我能不能实现,看了下效果确实不错,也还比较有新意,复杂度也不是非常高,所以就花时间给做了,我们先一起看下原gif图效果:




    从效果上看,我们需要考虑以下几个问题:

    1.叶子的随机产生;

    2.叶子随着一条正余弦曲线移动;

    3.叶子在移动的时候旋转,旋转方向随机,正时针或逆时针;

    4.叶子遇到进度条,似乎是融合进入;

    5.叶子不能超出最左边的弧角;

    7.叶子飘出时的角度不是一致,走的曲线的振幅也有差别,否则太有规律性,缺乏美感;


    总的看起来,需要注意和麻烦的地方主要是以上几点,当然还有一些细节问题,比如最左边是圆弧等等;

    那接下来我们将效果进行分解,然后逐个击破:

    整个效果来说,我们需要的图主要是飞动的小叶子和右边旋转的风扇,其他的部分都可以用色值进行绘制,当然我们为了方便,就连底部框一起切了;

    先从gif 图里把飞动的小叶子和右边旋转的风扇、底部框抠出来,小叶子图如下:


                                              


    我们需要处理的主要有两个部分:

    1. 随着进度往前绘制的进度条;

    2. 不断飞出来的小叶片;


    我们先处理第一部分 - 随着进度往前绘制的进度条:

    进度条的位置根据外层传入的 progress 进行计算,可以分为图中 1、2、3 三个阶段:



    1. 当progress 较小,算出的当前距离还在弧形以内时,需要绘制如图所示 1 区域的弧形,其余部分用白色填充;

    2. 当 progress 算出的距离到2时,需要绘制棕色半圆弧形,其余部分用白色矩形填充;

    3. 当 progress 算出的距离到3 时,需要绘制棕色半圆弧形,棕色矩形,白色矩形;

    4. 当 progress 算出的距离到头时,需要绘制棕色半圆弧形,棕色矩形;(可以合并到3中)


    首先根据进度条的宽度和当前进度、总进度算出当前的位置:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. //mProgressWidth为进度条的宽度,根据当前进度算出进度条的位置  
    2. mCurrentProgressPosition = mProgressWidth * mProgress / TOTAL_PROGRESS;  


    然后按照上面的逻辑进行绘制,其中需要计算上图中的红色弧角角度,计算方法如下:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. // 单边角度  
    2. int angle = (int) Math.toDegrees(Math.acos((mArcRadius - mCurrentProgressPosition)/ (float) mArcRadius));  
    Math.acos()  -反余弦函数;

    Math.toDegrees() - 弧度转化为角度,Math.toRadians 角度转化为弧度

    所以圆弧的起始点为:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. int startAngle = 180 - angle;  


    圆弧划过的角度为:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. 2 * angle  


    这一块的代码如下:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. // mProgressWidth为进度条的宽度,根据当前进度算出进度条的位置  
    2. mCurrentProgressPosition = mProgressWidth * mProgress / TOTAL_PROGRESS;  
    3. // 即当前位置在图中所示1范围内  
    4. if (mCurrentProgressPosition < mArcRadius) {  
    5.     Log.i(TAG, "mProgress = " + mProgress + "---mCurrentProgressPosition = "  
    6.             + mCurrentProgressPosition  
    7.             + "--mArcProgressWidth" + mArcRadius);  
    8.     // 1.绘制白色ARC,绘制orange ARC  
    9.     // 2.绘制白色矩形  
    10.   
    11.     // 1.绘制白色ARC  
    12.     canvas.drawArc(mArcRectF, 90, 180, false, mWhitePaint);  
    13.   
    14.     // 2.绘制白色矩形  
    15.     mWhiteRectF.left = mArcRightLocation;  
    16.     canvas.drawRect(mWhiteRectF, mWhitePaint);  
    17.   
    18.     // 3.绘制棕色 ARC  
    19.     // 单边角度  
    20.     int angle = (int) Math.toDegrees(Math.acos((mArcRadius - mCurrentProgressPosition)  
    21.             / (float) mArcRadius));  
    22.     // 起始的位置  
    23.     int startAngle = 180 - angle;  
    24.     // 扫过的角度  
    25.     int sweepAngle = 2 * angle;  
    26.     Log.i(TAG, "startAngle = " + startAngle);  
    27.     canvas.drawArc(mArcRectF, startAngle, sweepAngle, false, mOrangePaint);  
    28. } else {  
    29.     Log.i(TAG, "mProgress = " + mProgress + "---transfer-----mCurrentProgressPosition = "  
    30.             + mCurrentProgressPosition  
    31.             + "--mArcProgressWidth" + mArcRadius);  
    32.     // 1.绘制white RECT  
    33.     // 2.绘制Orange ARC  
    34.     // 3.绘制orange RECT  
    35.      
    36.     // 1.绘制white RECT  
    37.     mWhiteRectF.left = mCurrentProgressPosition;  
    38.     canvas.drawRect(mWhiteRectF, mWhitePaint);  
    39.       
    40.     // 2.绘制Orange ARC  
    41.     canvas.drawArc(mArcRectF, 90, 180, false, mOrangePaint);  
    42.     // 3.绘制orange RECT  
    43.     mOrangeRectF.left = mArcRightLocation;  
    44.     mOrangeRectF.right = mCurrentProgressPosition;  
    45.     canvas.drawRect(mOrangeRectF, mOrangePaint);  
    46.   
    47. }  


    接下来再来看叶子部分:

    首先根据效果情况基本确定出 曲线函数,标准函数方程为:y = A(wx+Q)+h,其中w影响周期,A影响振幅 ,周期T= 2 * Math.PI/w;

    根据效果可以看出,周期大致为总进度长度,所以确定w=(float) ((float) 2 * Math.PI /mProgressWidth);


    仔细观察效果,我们可以发现,叶子飘动的过程中振幅不是完全一致的,产生一种错落的效果,既然如此,我们给叶子定义一个Type,根据Type 确定不同的振幅;

    我们创建一个叶子对象:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. private class Leaf {  
    2.   
    3.      // 在绘制部分的位置  
    4.      float x, y;  
    5.      // 控制叶子飘动的幅度  
    6.      StartType type;  
    7.      // 旋转角度  
    8.      int rotateAngle;  
    9.      // 旋转方向--0代表顺时针,1代表逆时针  
    10.      int rotateDirection;  
    11.      // 起始时间(ms)  
    12.      long startTime;  
    13.  }  

    类型采用枚举进行定义,其实就是用来区分不同的振幅:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. private enum StartType {  
    2.     LITTLE, MIDDLE, BIG  
    3. }  

    创建一个LeafFactory类用于创建一个或多个叶子信息:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. private class LeafFactory {  
    2.     private static final int MAX_LEAFS = 6;  
    3.     Random random = new Random();  
    4.   
    5.     // 生成一个叶子信息  
    6.     public Leaf generateLeaf() {  
    7.         Leaf leaf = new Leaf();  
    8.         int randomType = random.nextInt(3);  
    9.         // 随时类型- 随机振幅  
    10.         StartType type = StartType.MIDDLE;  
    11.         switch (randomType) {  
    12.             case 0:  
    13.                 break;  
    14.             case 1:  
    15.                 type = StartType.LITTLE;  
    16.                 break;  
    17.             case 2:  
    18.                 type = StartType.BIG;  
    19.                 break;  
    20.             default:  
    21.                 break;  
    22.         }  
    23.         leaf.type = type;  
    24.         // 随机起始的旋转角度  
    25.         leaf.rotateAngle = random.nextInt(360);  
    26.         // 随机旋转方向(顺时针或逆时针)  
    27.         leaf.rotateDirection = random.nextInt(2);  
    28.         // 为了产生交错的感觉,让开始的时间有一定的随机性  
    29.         mAddTime += random.nextInt((int) (LEAF_FLOAT_TIME * 1.5));  
    30.         leaf.startTime = System.currentTimeMillis() + mAddTime;  
    31.         return leaf;  
    32.     }  
    33.   
    34.     // 根据最大叶子数产生叶子信息  
    35.     public List<Leaf> generateLeafs() {  
    36.         return generateLeafs(MAX_LEAFS);  
    37.     }  
    38.   
    39.     // 根据传入的叶子数量产生叶子信息  
    40.     public List<Leaf> generateLeafs(int leafSize) {  
    41.         List<Leaf> leafs = new LinkedList<Leaf>();  
    42.         for (int i = 0; i < leafSize; i++) {  
    43.             leafs.add(generateLeaf());  
    44.         }  
    45.         return leafs;  
    46.     }  
    47. }  

    定义两个常亮分别记录中等振幅和之间的振幅差:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. // 中等振幅大小  
    2. private static final int MIDDLE_AMPLITUDE = 13;  
    3. // 不同类型之间的振幅差距  
    4. private static final int AMPLITUDE_DISPARITY = 5;  
    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. // 中等振幅大小  
    2. private int mMiddleAmplitude = MIDDLE_AMPLITUDE;  
    3. // 振幅差  
    4. private int mAmplitudeDisparity = AMPLITUDE_DISPARITY;  

    有了以上信息,我们则可以获取到叶子的Y值:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. // 通过叶子信息获取当前叶子的Y值  
    2. private int getLocationY(Leaf leaf) {  
    3.     // y = A(wx+Q)+h  
    4.     float w = (float) ((float) 2 * Math.PI / mProgressWidth);  
    5.     float a = mMiddleAmplitude;  
    6.     switch (leaf.type) {  
    7.         case LITTLE:  
    8.             // 小振幅 = 中等振幅 - 振幅差  
    9.             a = mMiddleAmplitude - mAmplitudeDisparity;  
    10.             break;  
    11.         case MIDDLE:  
    12.             a = mMiddleAmplitude;  
    13.             break;  
    14.         case BIG:  
    15.             // 小振幅 = 中等振幅 + 振幅差  
    16.             a = mMiddleAmplitude + mAmplitudeDisparity;  
    17.             break;  
    18.         default:  
    19.             break;  
    20.     }  
    21.     Log.i(TAG, "---a = " + a + "---w = " + w + "--leaf.x = " + leaf.x);  
    22.     return (int) (a * Math.sin(w * leaf.x)) + mArcRadius * 2 / 3;  
    23. }  

    接下来,我们开始绘制叶子:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. /**  
    2.  * 绘制叶子  
    3.  *   
    4.  * @param canvas  
    5.  */  
    6. private void drawLeafs(Canvas canvas) {  
    7.     long currentTime = System.currentTimeMillis();  
    8.     for (int i = 0; i < mLeafInfos.size(); i++) {  
    9.         Leaf leaf = mLeafInfos.get(i);  
    10.         if (currentTime > leaf.startTime && leaf.startTime != 0) {  
    11.             // 绘制叶子--根据叶子的类型和当前时间得出叶子的(x,y)  
    12.             getLeafLocation(leaf, currentTime);  
    13.             // 根据时间计算旋转角度  
    14.             canvas.save();  
    15.             // 通过Matrix控制叶子旋转  
    16.             Matrix matrix = new Matrix();  
    17.             float transX = mLeftMargin + leaf.x;  
    18.             float transY = mLeftMargin + leaf.y;  
    19.             Log.i(TAG, "left.x = " + leaf.x + "--leaf.y=" + leaf.y);  
    20.             matrix.postTranslate(transX, transY);  
    21.             // 通过时间关联旋转角度,则可以直接通过修改LEAF_ROTATE_TIME调节叶子旋转快慢  
    22.             float rotateFraction = ((currentTime - leaf.startTime) % LEAF_ROTATE_TIME)  
    23.                     / (float) LEAF_ROTATE_TIME;  
    24.             int angle = (int) (rotateFraction * 360);  
    25.             // 根据叶子旋转方向确定叶子旋转角度  
    26.             int rotate = leaf.rotateDirection == 0 ? angle + leaf.rotateAngle : -angle  
    27.                     + leaf.rotateAngle;  
    28.             matrix.postRotate(rotate, transX  
    29.                     + mLeafWidth / 2, transY + mLeafHeight / 2);  
    30.             canvas.drawBitmap(mLeafBitmap, matrix, mBitmapPaint);  
    31.             canvas.restore();  
    32.         } else {  
    33.             continue;  
    34.         }  
    35.     }  
    36. }  

    最后,向外层暴露几个接口:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. /**  
    2.  * 设置中等振幅  
    3.  *   
    4.  * @param amplitude  
    5.  */  
    6. public void setMiddleAmplitude(int amplitude) {  
    7.     this.mMiddleAmplitude = amplitude;  
    8. }  
    9.   
    10. /**  
    11.  * 设置振幅差  
    12.  *   
    13.  * @param disparity  
    14.  */  
    15. public void setMplitudeDisparity(int disparity) {  
    16.     this.mAmplitudeDisparity = disparity;  
    17. }  
    18.   
    19. /**  
    20.  * 获取中等振幅  
    21.  *   
    22.  * @param amplitude  
    23.  */  
    24. public int getMiddleAmplitude() {  
    25.     return mMiddleAmplitude;  
    26. }  
    27.   
    28. /**  
    29.  * 获取振幅差  
    30.  *   
    31.  * @param disparity  
    32.  */  
    33. public int getMplitudeDisparity() {  
    34.     return mAmplitudeDisparity;  
    35. }  
    36.   
    37. /**  
    38.  * 设置进度  
    39.  *   
    40.  * @param progress  
    41.  */  
    42. public void setProgress(int progress) {  
    43.     this.mProgress = progress;  
    44.     postInvalidate();  
    45. }  
    46.   
    47. /**  
    48.  * 设置叶子飘完一个周期所花的时间  
    49.  *   
    50.  * @param time  
    51.  */  
    52. public void setLeafFloatTime(long time) {  
    53.     this.mLeafFloatTime = time;  
    54. }  
    55.   
    56. /**  
    57.  * 设置叶子旋转一周所花的时间  
    58.  *   
    59.  * @param time  
    60.  */  
    61. public void setLeafRotateTime(long time) {  
    62.     this.mLeafRotateTime = time;  

    这些接口用来干嘛呢?用于把我们的动效做成完全可手动调节的,这样做有什么好处呢?

    1. 更加便于产品、射鸡湿查看效果,避免YY,自己手动调节,不会出现要你一遍遍的改参数安装、查看、再改、再查看... ... N遍之后说 “这好像不是我想要的” -- 瞬间天崩地裂,天昏地暗,感觉被全世界抛弃;

    2. 便于体现你是一个考虑全面,思维缜密,会编程、会设计的艺术家,当然这纯属YY,主要还是方便大家;


    如此一来,射鸡湿们只需要不断的调节即可实时的看到展现的效果,最后只需要把最终的参数反馈过来即可,万事大吉,一了百了;

    当然,如果对方是个漂亮的妹子,而你又苦于没有机会搭讪,以上内容就当我没说,尽情的不按要求写吧,她肯定会主动找你的,说不定连饭都反过来请了... ...


    好啦,言归正传,完成收尾部分,我们让所有的参数都可调节起来:

    把剩下的layout 和activity贴出来:

    activity:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. public class LeafLoadingActivity extends Activity implements OnSeekBarChangeListener,  
    2.         OnClickListener {  
    3.   
    4.     Handler mHandler = new Handler() {  
    5.         public void handleMessage(Message msg) {  
    6.             switch (msg.what) {  
    7.                 case REFRESH_PROGRESS:  
    8.                     if (mProgress < 40) {  
    9.                         mProgress += 1;  
    10.                         // 随机800ms以内刷新一次  
    11.                         mHandler.sendEmptyMessageDelayed(REFRESH_PROGRESS,  
    12.                                 new Random().nextInt(800));  
    13.                         mLeafLoadingView.setProgress(mProgress);  
    14.                     } else {  
    15.                         mProgress += 1;  
    16.                         // 随机1200ms以内刷新一次  
    17.                         mHandler.sendEmptyMessageDelayed(REFRESH_PROGRESS,  
    18.                                 new Random().nextInt(1200));  
    19.                         mLeafLoadingView.setProgress(mProgress);  
    20.   
    21.                     }  
    22.                     break;  
    23.   
    24.                 default:  
    25.                     break;  
    26.             }  
    27.         };  
    28.     };  
    29.   
    30.     private static final int REFRESH_PROGRESS = 0x10;  
    31.     private LeafLoadingView mLeafLoadingView;  
    32.     private SeekBar mAmpireSeekBar;  
    33.     private SeekBar mDistanceSeekBar;  
    34.     private TextView mMplitudeText;  
    35.     private TextView mDisparityText;  
    36.     private View mFanView;  
    37.     private Button mClearButton;  
    38.     private int mProgress = 0;  
    39.   
    40.     private TextView mProgressText;  
    41.     private View mAddProgress;  
    42.     private SeekBar mFloatTimeSeekBar;  
    43.   
    44.     private SeekBar mRotateTimeSeekBar;  
    45.     private TextView mFloatTimeText;  
    46.     private TextView mRotateTimeText;  
    47.   
    48.     @Override  
    49.     protected void onCreate(Bundle savedInstanceState) {  
    50.         super.onCreate(savedInstanceState);  
    51.         setContentView(R.layout.leaf_loading_layout);  
    52.         initViews();  
    53.         mHandler.sendEmptyMessageDelayed(REFRESH_PROGRESS, 3000);  
    54.     }  
    55.   
    56.     private void initViews() {  
    57.         mFanView = findViewById(R.id.fan_pic);  
    58.         RotateAnimation rotateAnimation = DXAnimationUtils.initRotateAnimation(false, 1500, true,  
    59.                 Animation.INFINITE);  
    60.         mFanView.startAnimation(rotateAnimation);  
    61.         mClearButton = (Button) findViewById(R.id.clear_progress);  
    62.         mClearButton.setOnClickListener(this);  
    63.   
    64.         mLeafLoadingView = (LeafLoadingView) findViewById(R.id.leaf_loading);  
    65.         mMplitudeText = (TextView) findViewById(R.id.text_ampair);  
    66.         mMplitudeText.setText(getString(R.string.current_mplitude,  
    67.                 mLeafLoadingView.getMiddleAmplitude()));  
    68.   
    69.         mDisparityText = (TextView) findViewById(R.id.text_disparity);  
    70.         mDisparityText.setText(getString(R.string.current_Disparity,  
    71.                 mLeafLoadingView.getMplitudeDisparity()));  
    72.   
    73.         mAmpireSeekBar = (SeekBar) findViewById(R.id.seekBar_ampair);  
    74.         mAmpireSeekBar.setOnSeekBarChangeListener(this);  
    75.         mAmpireSeekBar.setProgress(mLeafLoadingView.getMiddleAmplitude());  
    76.         mAmpireSeekBar.setMax(50);  
    77.   
    78.         mDistanceSeekBar = (SeekBar) findViewById(R.id.seekBar_distance);  
    79.         mDistanceSeekBar.setOnSeekBarChangeListener(this);  
    80.         mDistanceSeekBar.setProgress(mLeafLoadingView.getMplitudeDisparity());  
    81.         mDistanceSeekBar.setMax(20);  
    82.   
    83.         mAddProgress = findViewById(R.id.add_progress);  
    84.         mAddProgress.setOnClickListener(this);  
    85.         mProgressText = (TextView) findViewById(R.id.text_progress);  
    86.   
    87.         mFloatTimeText = (TextView) findViewById(R.id.text_float_time);  
    88.         mFloatTimeSeekBar = (SeekBar) findViewById(R.id.seekBar_float_time);  
    89.         mFloatTimeSeekBar.setOnSeekBarChangeListener(this);  
    90.         mFloatTimeSeekBar.setMax(5000);  
    91.         mFloatTimeSeekBar.setProgress((int) mLeafLoadingView.getLeafFloatTime());  
    92.         mFloatTimeText.setText(getResources().getString(R.string.current_float_time,  
    93.                 mLeafLoadingView.getLeafFloatTime()));  
    94.   
    95.         mRotateTimeText = (TextView) findViewById(R.id.text_rotate_time);  
    96.         mRotateTimeSeekBar = (SeekBar) findViewById(R.id.seekBar_rotate_time);  
    97.         mRotateTimeSeekBar.setOnSeekBarChangeListener(this);  
    98.         mRotateTimeSeekBar.setMax(5000);  
    99.         mRotateTimeSeekBar.setProgress((int) mLeafLoadingView.getLeafRotateTime());  
    100.         mRotateTimeText.setText(getResources().getString(R.string.current_float_time,  
    101.                 mLeafLoadingView.getLeafRotateTime()));  
    102.     }  
    103.   
    104.     @Override  
    105.     public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {  
    106.         if (seekBar == mAmpireSeekBar) {  
    107.             mLeafLoadingView.setMiddleAmplitude(progress);  
    108.             mMplitudeText.setText(getString(R.string.current_mplitude,  
    109.                     progress));  
    110.         } else if (seekBar == mDistanceSeekBar) {  
    111.             mLeafLoadingView.setMplitudeDisparity(progress);  
    112.             mDisparityText.setText(getString(R.string.current_Disparity,  
    113.                     progress));  
    114.         } else if (seekBar == mFloatTimeSeekBar) {  
    115.             mLeafLoadingView.setLeafFloatTime(progress);  
    116.             mFloatTimeText.setText(getResources().getString(R.string.current_float_time,  
    117.                     progress));  
    118.         }  
    119.         else if (seekBar == mRotateTimeSeekBar) {  
    120.             mLeafLoadingView.setLeafRotateTime(progress);  
    121.             mRotateTimeText.setText(getResources().getString(R.string.current_rotate_time,  
    122.                     progress));  
    123.         }  
    124.   
    125.     }  
    126.   
    127.     @Override  
    128.     public void onStartTrackingTouch(SeekBar seekBar) {  
    129.   
    130.     }  
    131.   
    132.     @Override  
    133.     public void onStopTrackingTouch(SeekBar seekBar) {  
    134.   
    135.     }  
    136.   
    137.     @Override  
    138.     public void onClick(View v) {  
    139.         if (v == mClearButton) {  
    140.             mLeafLoadingView.setProgress(0);  
    141.             mHandler.removeCallbacksAndMessages(null);  
    142.             mProgress = 0;  
    143.         } else if (v == mAddProgress) {  
    144.             mProgress++;  
    145.             mLeafLoadingView.setProgress(mProgress);  
    146.             mProgressText.setText(String.valueOf(mProgress));  
    147.         }  
    148.     }  
    149. }  

    layout:

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
    1. <?xml version="1.0" encoding="utf-8"?>  
    2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    3.     android:layout_width="match_parent"  
    4.     android:layout_height="match_parent"  
    5.     android:background="#fed255"  
    6.     android:orientation="vertical" >  
    7.   
    8.     <TextView  
    9.         android:layout_width="wrap_content"  
    10.         android:layout_height="wrap_content"  
    11.         android:layout_gravity="center_horizontal"  
    12.         android:layout_marginTop="100dp"  
    13.         android:text="loading ..."  
    14.         android:textColor="#FFA800"  
    15.         android:textSize=" 30dp" />  
    16.   
    17.     <RelativeLayout  
    18.         android:id="@+id/leaf_content"  
    19.         android:layout_width="match_parent"  
    20.         android:layout_height="wrap_content"  
    21.         android:layout_marginTop="50dp" >  
    22.   
    23.         <com.baidu.batterysaverDemo.ui.LeafLoadingView  
    24.             android:id="@+id/leaf_loading"  
    25.             android:layout_width="302dp"  
    26.             android:layout_height="61dp"  
    27.             android:layout_centerHorizontal="true" />  
    28.   
    29.         <ImageView  
    30.             android:id="@+id/fan_pic"  
    31.             android:layout_width="wrap_content"  
    32.             android:layout_height="wrap_content"  
    33.             android:layout_alignParentRight="true"  
    34.             android:layout_centerVertical="true"  
    35.             android:layout_marginRight="35dp"  
    36.             android:src="@drawable/fengshan" />  
    37.     </RelativeLayout>  
    38.   
    39.     <ScrollView  
    40.         android:layout_width="match_parent"  
    41.         android:layout_height="match_parent" >  
    42.   
    43.         <LinearLayout  
    44.             android:layout_width="match_parent"  
    45.             android:layout_height="match_parent"  
    46.             android:orientation="vertical" >  
    47.   
    48.             <LinearLayout  
    49.                 android:id="@+id/seek_content_one"  
    50.                 android:layout_width="match_parent"  
    51.                 android:layout_height="wrap_content"  
    52.                 android:layout_marginLeft="15dp"  
    53.                 android:layout_marginRight="15dp"  
    54.                 android:layout_marginTop="15dp" >  
    55.   
    56.                 <TextView  
    57.                     android:id="@+id/text_ampair"  
    58.                     android:layout_width="wrap_content"  
    59.                     android:layout_height="wrap_content"  
    60.                     android:layout_gravity="center_vertical"  
    61.                     android:textColor="#ffffa800"  
    62.                     android:textSize="15dp" />  
    63.   
    64.                 <SeekBar  
    65.                     android:id="@+id/seekBar_ampair"  
    66.                     android:layout_width="0dp"  
    67.                     android:layout_height="wrap_content"  
    68.                     android:layout_marginLeft="5dp"  
    69.                     android:layout_weight="1" />  
    70.             </LinearLayout>  
    71.   
    72.             <LinearLayout  
    73.                 android:layout_width="match_parent"  
    74.                 android:layout_height="wrap_content"  
    75.                 android:layout_marginLeft="15dp"  
    76.                 android:layout_marginRight="15dp"  
    77.                 android:layout_marginTop="15dp"  
    78.                 android:orientation="horizontal" >  
    79.   
    80.                 <TextView  
    81.                     android:id="@+id/text_disparity"  
    82.                     android:layout_width="wrap_content"  
    83.                     android:layout_height="wrap_content"  
    84.                     android:layout_gravity="center_vertical"  
    85.                     android:textColor="#ffffa800"  
    86.                     android:textSize="15dp" />  
    87.   
    88.                 <SeekBar  
    89.                     android:id="@+id/seekBar_distance"  
    90.                     android:layout_width="0dp"  
    91.                     android:layout_height="wrap_content"  
    92.                     android:layout_marginLeft="5dp"  
    93.                     android:layout_weight="1" />  
    94.             </LinearLayout>  
    95.   
    96.             <LinearLayout  
    97.                 android:layout_width="match_parent"  
    98.                 android:layout_height="wrap_content"  
    99.                 android:layout_marginLeft="15dp"  
    100.                 android:layout_marginRight="15dp"  
    101.                 android:layout_marginTop="15dp"  
    102.                 android:orientation="horizontal" >  
    103.   
    104.                 <TextView  
    105.                     android:id="@+id/text_float_time"  
    106.                     android:layout_width="wrap_content"  
    107.                     android:layout_height="wrap_content"  
    108.                     android:layout_gravity="center_vertical"  
    109.                     android:textColor="#ffffa800"  
    110.                     android:textSize="15dp" />  
    111.   
    112.                 <SeekBar  
    113.                     android:id="@+id/seekBar_float_time"  
    114.                     android:layout_width="0dp"  
    115.                     android:layout_height="wrap_content"  
    116.                     android:layout_marginLeft="5dp"  
    117.                     android:layout_weight="1" />  
    118.             </LinearLayout>  
    119.   
    120.             <LinearLayout  
    121.                 android:layout_width="match_parent"  
    122.                 android:layout_height="wrap_content"  
    123.                 android:layout_marginLeft="15dp"  
    124.                 android:layout_marginRight="15dp"  
    125.                 android:layout_marginTop="15dp"  
    126.                 android:orientation="horizontal" >  
    127.   
    128.                 <TextView  
    129.                     android:id="@+id/text_rotate_time"  
    130.                     android:layout_width="wrap_content"  
    131.                     android:layout_height="wrap_content"  
    132.                     android:layout_gravity="center_vertical"  
    133.                     android:textColor="#ffffa800"  
    134.                     android:textSize="15dp" />  
    135.   
    136.                 <SeekBar  
    137.                     android:id="@+id/seekBar_rotate_time"  
    138.                     android:layout_width="0dp"  
    139.                     android:layout_height="wrap_content"  
    140.                     android:layout_marginLeft="5dp"  
    141.                     android:layout_weight="1" />  
    142.             </LinearLayout>  
    143.   
    144.             <Button  
    145.                 android:id="@+id/clear_progress"  
    146.                 android:layout_width="match_parent"  
    147.                 android:layout_height="wrap_content"  
    148.                 android:layout_marginTop="15dp"  
    149.                 android:text="去除进度条,玩转弧线"  
    150.                 android:textSize="18dp" />  
    151.   
    152.             <LinearLayout  
    153.                 android:layout_width="match_parent"  
    154.                 android:layout_height="wrap_content"  
    155.                 android:layout_marginLeft="15dp"  
    156.                 android:layout_marginRight="15dp"  
    157.                 android:layout_marginTop="15dp"  
    158.                 android:orientation="horizontal" >  
    159.   
    160.                 <Button  
    161.                     android:id="@+id/add_progress"  
    162.                     android:layout_width="wrap_content"  
    163.                     android:layout_height="wrap_content"  
    164.                     android:text="增加进度: "  
    165.                     android:textSize="18dp" />  
    166.   
    167.                 <TextView  
    168.                     android:id="@+id/text_progress"  
    169.                     android:layout_width="wrap_content"  
    170.                     android:layout_height="wrap_content"  
    171.                     android:layout_gravity="center_vertical"  
    172.                     android:textColor="#ffffa800"  
    173.                     android:textSize="15dp" />  
    174.             </LinearLayout>  
    175.         </LinearLayout>  
    176.     </ScrollView>  
    177.   
    178. </LinearLayout>  

    最终效果如下,本来录了20+s,但是PS只能转5s,所以有兴趣的大家自己运行的玩吧:





    源码CSDN下载地址:http://download.csdn.net/detail/tianjian4592/8524539

    展开全文
  • Android星体动效实现

    2020-07-28 23:32:53
    Android星体动效实现
  • 2019独角兽企业重金招聘Python工程师标准>>> ...

    背景:之前偶然看到优酷有类似的页面切换动画效果。于是自己也打算来实现下这样的效果。

    动效说明:点击界面中的任意位置,界面以点击位置作为中心点,开始以漩涡状态,扭曲,收缩。直到消失。

    直接上我实现的效果:

     

    一,方法原理说明:

    1.  将页面生成bitmap。
    2.  使用自定义View来绘制扭曲的图像。 图像绘制的时候使用的关键的api 是: canvas.drawBitmapMesh();

    二,实现细节说明:

        1.  生成页面Bitmap: 优先使用drawingCache , 如果没有再创建bitmap 对象。

    public static Bitmap createBitmapFromView(View view) {
            if (view instanceof ImageView) {
                Drawable drawable = ((ImageView) view).getDrawable();
                if (drawable != null && drawable instanceof BitmapDrawable) {
                    return ((BitmapDrawable) drawable).getBitmap();
                }
            }
            view.clearFocus();
            Bitmap bitmap = view.getDrawingCache();
            if(bitmap != null) {
                return bitmap;
            }
            
            bitmap = createBitmapSafely(view.getWidth(),
                    view.getHeight(), Bitmap.Config.ARGB_8888, 1);
            if (bitmap != null) {
                synchronized (sCanvas) {
                    Canvas canvas = sCanvas;
                    canvas.setBitmap(bitmap);
                    view.draw(canvas);
                    canvas.setBitmap(null);
                }
            }
            return bitmap;
        }
    
        public static Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int retryCount) {
            ...
        }

     

        2. 关于自定义控件 VortexView 。 主要是再onDraw(Canvas ) 方法中使用rootView 生成的Bitmap 通过canvas.drawBitmapMesh 方法来绘制扭曲的图像。(最开始我的方案是支持在native 中,对图片进行像素级别的修改。 虽然成功了,但是效率却很慢。)

    关于API drawBitmapMesh 可以参考一下这篇博文:使用drawBitmapMesh方法产生水波

    期原理猜测应该是使用了opengl 中纹理,坐标变换映射的技术。(只是猜测)

    drawBitmapMesh使用方法:将原始图片分割成为M行,N列。 并计算得出原始的每个交点再二维空间内的坐标。 坐上角为(0,0)点。 水平向右为X正方向。 垂直向下为Y正方向。  使用漩涡算法,计算每一帧下,原始顶点(线的交点)在当前时刻下的坐标位置。即生成的局部变量ve[]; 这样界面就能显示出图像被扭曲的动画。

    当然:分割的行列越多,效果就会越好。

    public class VortextView extends View {
    ...
    
     @Override
        public void onDraw(Canvas canvas) {
            if (destBitmap != null) {
                int mswd = mesh.getMeshWidth();
                int msht = mesh.getMeshHeight();
                float[] ve = mesh.getVertices();
    
                if (rect != null) {
                    int count = canvas.save();
                    canvas.translate(rect.left, rect.top);
                    canvas.drawBitmapMesh(destBitmap, mswd, msht, ve, 0, null, 0, null);
                    canvas.restoreToCount(count);
                } else {
                    canvas.drawBitmapMesh(destBitmap, mswd, msht, ve, 0, null, 0, null);
                }
    
    //            mesh.drawLines(canvas,paint);
    
            }
        }
    ...
    }

        3. 关于算法:不管是漩涡扭曲动效,还是仿造mac os 最小化效果。 原理都是一致的。唯一不同的地方在于算法。我们需要分别设计算法来拟合出在目标时刻下新的顶点位置。

    • 漩涡动效算法:这里需要用到极坐标公式。夹角随着时间增大而增大。半径随着时间增大而见小。然后在求出对应的x和y;
    • mac os 最小化:这里我使用了2阶贝塞尔曲线。

     

    转载于:https://my.oschina.net/u/592116/blog/2877760

    展开全文
  • android动效开篇

    万次阅读 多人点赞 2015-12-10 15:26:45
    在现在的Android App开发中,动效越来越受到产品和设计师同学的重视,如此一来,也就增大了对开发同学的考验,虽说简单的动效:如移动,旋转,缩放,渐变或普通的界面跳转相对简单,但在目前日益激烈的竞争条件下,...
  • Android高级绘图之复杂动效开发

    千次阅读 2017-08-22 17:56:40
    最近在学习Android有关绘图的开发,有幸找到这位作者的博客和教学,大受启发,现将其整理如下,方便查看和后续学习:作者CSDN博客:http://my.csdn.net/tianjian4592 作者GIT地址:...
  • Android 仿苹果siri语音动效

    千次阅读 2018-10-30 11:19:22
    import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import ...
  • Android 星体动效实现

    2016-11-22 09:35:55
    Android星空动效的实现 首先,要实现这样一幅动效图,基本上就是两个步骤:  第一,在背景上确定物体(图片中的星体,流星)的初始位置  第二,使物体进行移动,在此过程中要实时更新物体的参数信息,比如大小...
  • 注:因部分原因,本篇主要讲解动效分析的思路,不提供源码下载,请见谅 ... ... 上一篇只讲了Canvas中的drawBitmap方法,并且还说的这个方法好像很腻害、能做出很多牛逼效果的样子,接下来这篇文章只是为了作为上...
  • 忙碌的工作终于可以停息一段时间了,最近突然有一个想法,就是自己写一个app,所以找了一些合适开源控件,这样更加...1.Side-Menu.Android 分类侧滑菜单,Yalantis 出品。 项目地址:https://github.com/Yalantis/Sid
  • Android可自定义动效的卡片切换视图

    千次阅读 2017-04-24 13:30:04
    转载请注明出处InfiniteCards可...animType : 动效展示类型 front : 将点击的卡片切换到第一个 switchPosition : 将点击的卡片和第一张卡片互换位置 frontToLast : 将第一张卡片移到最后,后面的卡片往前移动一个 car
  • Android studio 4.0 更新了动效开发神器MotionEditor
  • Android 动效系列之一 - AndroidSwipeLayout

    千次阅读 2016-04-19 21:45:02
    Android Swipe Layout 是国内优秀 Android 开发者 daimajia 的精彩作品。也绝对可以称得上是最强大的滑动布局(Swipe Layout)。这是另一个优秀项目 Android View Hover 的兄弟项目。目的是实现和 iOS 上类似的...
  • Android 之 Lottie 实现炫酷动画背后的原理

    千次阅读 多人点赞 2019-05-30 02:45:21
    这是程序亦非猿的第 78 期分享。作者 l 程序亦非猿来源 l 程序亦非猿(ID:chengxuyifeiyuan)转载请联系授权(微信ID:ONE-D-PIECE)0...
  • android动画效果大全

    千次阅读 2019-07-05 18:27:38
    import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatAc...
1 2 3 4 5 ... 20
收藏数 4,377
精华内容 1,750
关键字:

android动效