仿百度外卖的酷炫水波纹效果及解析

前言:在网上经常会看到别人写的一些开源项目,然后会惊叹于他们的写的效果,当然那些大神也会把代码放出来,然后供大家看,但是因为他们是自己写的,所以有些地方就是单纯的贴了代码,让大家自己去看。介于我前面动画方面比较薄弱,所以有些地方就要一边跟着敲代码,一边去网上查相关知识。所以就借这次机会。我来写下我最近学的动画效果及相关的知识。

---------------------------------------我是前言分割君-------------------------------------------

感谢CSDN的Zcoder2013,最后附上文字链接。

--------------------------------正文君打工的温州皮革厂倒闭了-------------------------------

仿百度外卖个人中心效果

仿百度外卖的个人中心

强烈建议看下自定义View的整个教程
从零起步,从入门到懵逼的自定义 View 教程
从零起步,从入门到懵逼的自定义 View 教程
从零起步,从入门到懵逼的自定义 View 教程
(重要的事情说三遍)(重要的事情说三遍)(重要的事情说三遍)

-----------------------------------分析君带着小姨子逃跑了-------------------------

我们先来看下这个自定义的View的代码是如何实现的。

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DrawFilter;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;



public class WaveView extends View{

    private Path mAbovePath,mBelowWavePath;
    private Paint mAboveWavePaint,mBelowWavePaint;
    private DrawFilter mDrawFilter;
    private float φ;
    private OnWaveAnimationListener mWaveAnimationListener;

    public WaveView(Context context, AttributeSet attrs) {
        super(context, attrs);

        //初始化路径
        mAbovePath = new Path();
        mBelowWavePath = new Path();

        //初始化画笔
        mAboveWavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mAboveWavePaint.setAntiAlias(true);
        mAboveWavePaint.setStyle(Paint.Style.FILL);
        mAboveWavePaint.setColor(Color.WHITE);

        mBelowWavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBelowWavePaint.setAntiAlias(true);
        mBelowWavePaint.setStyle(Paint.Style.FILL);
        mBelowWavePaint.setColor(Color.WHITE);
        mBelowWavePaint.setAlpha(80);

        //画布抗锯齿
        mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.setDrawFilter(mDrawFilter);

        mAbovePath.reset();
        mBelowWavePath.reset();

        φ-=0.1f;
        float y,y2;
        double ω = 2*Math.PI / getWidth();

        mAbovePath.moveTo(getLeft(),getBottom());
        mBelowWavePath.moveTo(getLeft(),getBottom());

        for (float x = 0; x <= getWidth(); x += 20) {
            /**
             *  y=Asin(ωx+φ)+k
             *  A—振幅越大,波形在y轴上最大与最小值的差值越大
             *  ω—角速度, 控制正弦周期(单位角度内震动的次数)
             *  φ—初相,反映在坐标系上则为图像的左右移动。这里通过不断改变φ,达到波浪移动效果
             *  k—偏距,反映在坐标系上则为图像的上移或下移。
             */
            y = (float) (8 * Math.cos(ω * x + φ) +8);
            y2 = (float) (8 * Math.sin(ω * x + φ));
            mAbovePath.lineTo(x, y);
            mBelowWavePath.lineTo(x, y2);
            //回调 把y坐标的值传出去(在activity里面接收让小机器人随波浪一起摇摆)
            mWaveAnimationListener.OnWaveAnimation(y);
        }

       
        mAbovePath.lineTo(getRight(),getBottom());
        mBelowWavePath.lineTo(getRight(),getBottom());

        canvas.drawPath(mAbovePath,mAboveWavePaint);
        canvas.drawPath(mBelowWavePath,mBelowWavePaint);

        postInvalidateDelayed(20);
    }

    public void setOnWaveAnimationListener(OnWaveAnimationListener l){
        this.mWaveAnimationListener = l;
    }

    public interface OnWaveAnimationListener{
        void OnWaveAnimation(float y);
    }
}

我们一步步来分析。首先我们要自定义一个View。

自定义View流程:

步骤 关键字 作用
1 构造函数 View初始化
2 onMeasure 测量View大小
3 onSizeChanged 确定View大小
4 onLayout 确定子View布局(自定义View包含子View时有用)
5 onDraw 实际绘制内容
6 提供接口 控制View或监听View某些状态。

我们来讲解下上图中的这几个我们等会会使用到的重要方法:

1.构造函数

构造函数是View的入口,可以用于初始化一些的内容,和获取自定义属性
View的构造函数有四种重载分别如下:

public void WaveView(Context context) {}
public void WaveView(Context context, AttributeSet attrs) {}
public void WaveView(Context context, AttributeSet attrs, int defStyleAttr) {}
public void WaveView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {}

有四个参数的构造函数在API21的时候才添加上,暂不考虑。
有三个参数的构造函数中第三个参数是默认的Style,这里的默认的Style是指它在当前Application或Activity所用的Theme中的默认Style,且只有在明确调用的时候才会生效,以系统中的ImageButton为例说明:

    public WaveView(Context context, AttributeSet attrs) {
        //调用了三个参数的构造函数,明确指定第三个参数
        this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
    }

    public WaveView(Context context, AttributeSet attrs, int defStyleAttr) {
        //此处调了四个参数的构造函数,无视即可
        this(context, attrs, defStyleAttr, 0); 
    }

注意:即使你在View中使用了Style这个属性也不会调用三个参数的构造函数,所调用的依旧是两个参数的构造函数。
由于三个参数的构造函数第三个参数一般不用,暂不考虑,第三个参数的具体用法会在以后用到的时候详细介绍。
排除了两个之后,只剩下一个参数和两个参数的构造函数,他们的详情如下:

  //一般在直接New一个View的时候调用。
  public void WaveView(Context context) {}

  //一般在layout文件中使用的时候会调用,关于它的所有属性(包括自定义属性)都会包含在attrs中传递进来。
  public void WaveView(Context context, AttributeSet attrs) {}
**以下方法调用的是一个参数的构造函数:**
//在Avtivity中 WaveView view = new WaveView(this);

以下方法调用的是两个参数的构造函数:

  //在layout文件中 - 格式为: 包名.View名
  <com.androiddemo.waveview.WaveView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

因为我们这个例子中是在layout中使用这个自定义View。所以我们当前例子里面,只需要留下二个参数的public void WaveView(Context context, AttributeSet attrs) {}构造函数即可。

2.绘制内容(onDraw)

onDraw是实际绘制的部分,也就是我们真正关心的部分,使用的是Canvas绘图。

@Override 
protected void onDraw(Canvas canvas) { 
     super.onDraw(canvas); 
}

我们是不是在想这个百度个人中心效果到底是怎么实现的,在这里我要贴个图:


sin函数及cos函数

哈哈。没错。那二个上下浮动的曲线。我们可以用同时画二个线,一个sin函数,一个cos函数。而且处于同一水平线。不就一个交错的波浪了。

类似这样的效果

好,第一步的大概思路咱们有了。咱们再思考如何画这些线呢。
这里先介绍几个基本知识点:

  • 了解Path:
    不清楚这个Path的一定要一定要一定要看下面这个Path基础链接
    Path之基本操作

官方介绍:
The Path class encapsulates compound (multiple contour) geometric paths consisting of straight line segments, quadratic curves, and cubic curves. It can be drawn with canvas.drawPath(path, paint), either filled or stroked (based on the paint's Style), or it can be used for clipping or to draw text on a path.
嗯,没错依旧是拿来装逼的,如果你看不懂的话,不用担心,其实并没有什么卵用。

通俗解释:
Path封装了由直线和曲线(二次,三次贝塞尔曲线)构成的几何路径。你能用Canvas中的drawPath来把这条路径画出来(同样支持Paint的不同绘制模式),也可以用于剪裁画布和根据路径绘制文字。我们有时会用Path来描述一个图像的轮廓,所以也会称为轮廓线(轮廓线仅是Path的一种使用方法,两者并不等价)

我就列举出我们这次的仿百度效果会使用的几个方法:

作用 相关方法 备注
移动起点 moveTo 移动下一次操作的起点位置
连接直线 lineTo 添加上一个点到当前点之间的直线到Path
重置路径 reset, rewind 清除Path中的内容reset不保留内部数据结构,但会保留FillType.****rewind会保留内部的数据结构,但不保留FillType

没错。为啥我列举了这几个方法呢。有人要问,lineTo不是画直线的么。其实这个sin和cos曲线就是被我们一小段一小段的用线段画出来的。

哈哈,纯手工画的
      /**
         *  y=Asin(ωx+φ)+k
         *  A—振幅越大,波形在y轴上最大与最小值的差值越大
         *  ω—角速度, 控制正弦周期(单位角度内震动的次数)
         *  φ—初相,反映在坐标系上则为图像的左右移动。这里通过不断改变φ,达到波浪移动效果
         *  k—偏距,反映在坐标系上则为图像的上移或下移。
         */

比如画上述这个sin函数。我们画好后。怎么让他不停的往左移动,产生波浪的效果呢。这时候就会想到重新绘制,然后再画一遍,但是这时候不能原来这个sin函数。sin里面的φ参数要变一下,这样再次重绘的时候。新画出来的sin线就是一个被左右方向移动后的线了。给你的感觉不就是像波浪一样往右边移动了!!!!

所以我们就知道了:(以sin为例)

  1. 画出用lineTo在X轴上画出一段段小的线段,拼成一个sin曲线图
  2. 画完这个曲线后重新执行绘图,这时候的改变sin函数内部参数,画出来的曲线已经在上一次的曲线的基础上被左右移动过了。

这下了解了这些。我们再仔细的分析下onDraw方法的代码:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.setDrawFilter(mDrawFilter);

        mAbovePath.reset();
        mBelowWavePath.reset();

        φ-=0.1f;
        float y,y2;
        double ω = 2*Math.PI / getWidth();

        mAbovePath.moveTo(getLeft(),getBottom());
        mBelowWavePath.moveTo(getLeft(),getBottom());

        for (float x = 0; x <= getWidth(); x += 20) {
            /**
             *  y=Asin(ωx+φ)+k
             *  A—振幅越大,波形在y轴上最大与最小值的差值越大
             *  ω—角速度, 控制正弦周期(单位角度内震动的次数)
             *  φ—初相,反映在坐标系上则为图像的左右移动。这里通过不断改变φ,达到波浪移动效果
             *  k—偏距,反映在坐标系上则为图像的上移或下移。
             */
            y = (float) (8 * Math.cos(ω * x + φ) +8);
            y2 = (float) (8 * Math.sin(ω * x + φ));
            mAbovePath.lineTo(x, y);
            mBelowWavePath.lineTo(x, y2);
            //回调 把y坐标的值传出去(在activity里面接收让小机器人随波浪一起摇摆)
            mWaveAnimationListener.OnWaveAnimation(y);
        }

       
        mAbovePath.lineTo(getRight(),getBottom());
        mBelowWavePath.lineTo(getRight(),getBottom());

        canvas.drawPath(mAbovePath,mAboveWavePaint);
        canvas.drawPath(mBelowWavePath,mBelowWavePaint);

        postInvalidateDelayed(20);
    }

第一步:我们可以看到它是先通过for循环

for (float x = 0; x <= getWidth(); x += 20) {
          
}

把这个绘画的曲线在X轴上分割成为一段段。每一段再用线段画出来就可以了。

又是丑丑的手工画图

而每一段的画又是要按照sin或者cos的函数来画。并且是通过lineTo方法来。所以最后合在一起就是:

for (float x = 0; x <= getWidth(); x += 20) {
            /**
             *  y=Asin(ωx+φ)+k
             *  A—振幅越大,波形在y轴上最大与最小值的差值越大
             *  ω—角速度, 控制正弦周期(单位角度内震动的次数)
             *  φ—初相,反映在坐标系上则为图像的左右移动。这里通过不断改变φ,达到波浪移动效果
             *  k—偏距,反映在坐标系上则为图像的上移或下移。
             */
            y = (float) (8 * Math.cos(ω * x + φ) +8);
            y2 = (float) (8 * Math.sin(ω * x + φ));
            mAbovePath.lineTo(x, y);
            mBelowWavePath.lineTo(x, y2);
        }

这时候如果我们canvas.drawPath方法来画出我们上面的这个处理过的path。就可以画出来相应的sin或者cos线了。

第二步:重新绘制曲线
在onDraw()方法的结尾处加上:
postInvalidateDelayed(20);
这个方法会再20毫秒后会重新调用onDraw()方法。
然后再onDraw()方法的开始部分。我们要把path重新置空:
mAbovePath.reset(); mBelowWavePath.reset();
然后改变Asin(ωx+φ)+k的(φ—初相)这个值:
φ-=0.1f;
从而再一次画出来的曲线就已经左右被移动过了。让你产生波浪的感觉。


好的,我们已经学完了那二个波浪的成(zhuang)功(B)实现了。如何来实现那个头像跟随着曲线一起动呢。其实很简单。刚才我们能画出曲线。是通过Path 的lineTo方法不断的传入相应的(x,y)坐标,从而画出一个个线段,从而拼成了曲线,那就是我们能拿到每个线段的Y轴坐标上的值。也就是:

y = (float) (8 * Math.cos(ω * x + φ) +8);
y2 = (float) (8 * Math.sin(ω * x + φ));

那我们只要:

  1. 拿到图片对象:
    imageView = (ImageView) findViewById(R.id.image);
  2. 把上面的曲线的y或者y1值拿过来,比如我拿的是y。
  3. 让imageView与它的父View之间的margin中的bottom属性值等于这个y的值就可以了(demo里面是y+2)。这样就不停的上下的浮动了。
lp.setMargins(0,0,0,(int)y+2); 
imageView.setLayoutParams(lp);

附上Activity及layout的代码:

Activity:

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Gravity;
import android.widget.FrameLayout;
import android.widget.ImageView;

import yunyuan.androiddemo.R;

/**
 * Created by willy on 16/12/12.
 */

public class WaveActivity extends AppCompatActivity {

    private ImageView imageView;
    private WaveView waveView3;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.act_waveview);
        imageView = (ImageView) findViewById(R.id.image);
        waveView3 = (WaveView) findViewById(R.id.wave_view);

        final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(-2,-2);
        lp.gravity = Gravity.BOTTOM|Gravity.CENTER;
        waveView3.setOnWaveAnimationListener(new WaveView.OnWaveAnimationListener() {
            @Override
            public void OnWaveAnimation(float y) {
                lp.setMargins(0,0,0,(int)y+2);
                imageView.setLayoutParams(lp);
            }
        });
    }
}

layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@android:color/holo_red_dark"
        >

        <yunyuan.androiddemo.waveview.WaveView
            android:id="@+id/wave_view"
            android:layout_width="match_parent"
            android:layout_height="15dp"
            android:layout_gravity="bottom" />

        <ImageView
            android:id="@+id/image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|center"
            android:background="@mipmap/ic_launcher" />

    </FrameLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="其他内容"
            android:textSize="24sp" />

    </LinearLayout>

</LinearLayout>

最后咱们做出来的效果图就是这样滴:


最后再次感谢大神感谢CSDN的Zcoder2013
文章链接:http://blog.csdn.net/u011507982/article/details/53414422

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,444评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,867评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,157评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,312评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,673评论 3 289
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,802评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,743评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,470评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,696评论 2 250
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,187评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,538评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,188评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,127评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,902评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,889评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,741评论 2 274

推荐阅读更多精彩内容