前言:在網(wǎng)上經(jīng)常會(huì)看到別人寫(xiě)的一些開(kāi)源項(xiàng)目,然后會(huì)驚嘆于他們的寫(xiě)的效果,當(dāng)然那些大神也會(huì)把代碼放出來(lái),然后供大家看,但是因?yàn)樗麄兪亲约簩?xiě)的,所以有些地方就是單純的貼了代碼,讓大家自己去看。介于我前面動(dòng)畫(huà)方面比較薄弱,所以有些地方就要一邊跟著敲代碼,一邊去網(wǎng)上查相關(guān)知識(shí)。所以就借這次機(jī)會(huì)。我來(lái)寫(xiě)下我最近學(xué)的動(dòng)畫(huà)效果及相關(guān)的知識(shí)。
---------------------------------------我是前言分割君-------------------------------------------
感謝CSDN的Zcoder2013,最后附上文字鏈接。
--------------------------------正文君打工的溫州皮革廠倒閉了-------------------------------
仿百度外賣個(gè)人中心效果
強(qiáng)烈建議看下自定義View的整個(gè)教程
從零起步,從入門到懵逼的自定義 View 教程
從零起步,從入門到懵逼的自定義 View 教程
從零起步,從入門到懵逼的自定義 View 教程
(重要的事情說(shuō)三遍)(重要的事情說(shuō)三遍)(重要的事情說(shuō)三遍)
-----------------------------------分析君帶著小姨子逃跑了-------------------------
我們先來(lái)看下這個(gè)自定義的View的代碼是如何實(shí)現(xiàn)的。
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();
//初始化畫(huà)筆
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);
//畫(huà)布抗鋸齒
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軸上最大與最小值的差值越大
* ω—角速度, 控制正弦周期(單位角度內(nèi)震動(dòng)的次數(shù))
* φ—初相,反映在坐標(biāo)系上則為圖像的左右移動(dòng)。這里通過(guò)不斷改變?chǔ)?達(dá)到波浪移動(dòng)效果
* k—偏距,反映在坐標(biāo)系上則為圖像的上移或下移。
*/
y = (float) (8 * Math.cos(ω * x + φ) +8);
y2 = (float) (8 * Math.sin(ω * x + φ));
mAbovePath.lineTo(x, y);
mBelowWavePath.lineTo(x, y2);
//回調(diào) 把y坐標(biāo)的值傳出去(在activity里面接收讓小機(jī)器人隨波浪一起搖擺)
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);
}
}
我們一步步來(lái)分析。首先我們要自定義一個(gè)View。
自定義View流程:
步驟 | 關(guān)鍵字 | 作用 |
---|---|---|
1 | 構(gòu)造函數(shù) | View初始化 |
2 | onMeasure | 測(cè)量View大小 |
3 | onSizeChanged | 確定View大小 |
4 | onLayout | 確定子View布局(自定義View包含子View時(shí)有用) |
5 | onDraw | 實(shí)際繪制內(nèi)容 |
6 | 提供接口 | 控制View或監(jiān)聽(tīng)View某些狀態(tài)。 |
我們來(lái)講解下上圖中的這幾個(gè)我們等會(huì)會(huì)使用到的重要方法:
1.構(gòu)造函數(shù)
構(gòu)造函數(shù)是View的入口,可以用于初始化一些的內(nèi)容,和獲取自定義屬性。
View的構(gòu)造函數(shù)有四種重載分別如下:
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) {}
有四個(gè)參數(shù)的構(gòu)造函數(shù)在API21的時(shí)候才添加上,暫不考慮。
有三個(gè)參數(shù)的構(gòu)造函數(shù)中第三個(gè)參數(shù)是默認(rèn)的Style,這里的默認(rèn)的Style是指它在當(dāng)前Application或Activity所用的Theme中的默認(rèn)Style,且只有在明確調(diào)用的時(shí)候才會(huì)生效,以系統(tǒng)中的ImageButton為例說(shuō)明:
public WaveView(Context context, AttributeSet attrs) {
//調(diào)用了三個(gè)參數(shù)的構(gòu)造函數(shù),明確指定第三個(gè)參數(shù)
this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
}
public WaveView(Context context, AttributeSet attrs, int defStyleAttr) {
//此處調(diào)了四個(gè)參數(shù)的構(gòu)造函數(shù),無(wú)視即可
this(context, attrs, defStyleAttr, 0);
}
注意:即使你在View中使用了Style這個(gè)屬性也不會(huì)調(diào)用三個(gè)參數(shù)的構(gòu)造函數(shù),所調(diào)用的依舊是兩個(gè)參數(shù)的構(gòu)造函數(shù)。
由于三個(gè)參數(shù)的構(gòu)造函數(shù)第三個(gè)參數(shù)一般不用,暫不考慮,第三個(gè)參數(shù)的具體用法會(huì)在以后用到的時(shí)候詳細(xì)介紹。
排除了兩個(gè)之后,只剩下一個(gè)參數(shù)和兩個(gè)參數(shù)的構(gòu)造函數(shù),他們的詳情如下:
//一般在直接New一個(gè)View的時(shí)候調(diào)用。
public void WaveView(Context context) {}
//一般在layout文件中使用的時(shí)候會(huì)調(diào)用,關(guān)于它的所有屬性(包括自定義屬性)都會(huì)包含在attrs中傳遞進(jìn)來(lái)。
public void WaveView(Context context, AttributeSet attrs) {}
**以下方法調(diào)用的是一個(gè)參數(shù)的構(gòu)造函數(shù):**
//在Avtivity中 WaveView view = new WaveView(this);
以下方法調(diào)用的是兩個(gè)參數(shù)的構(gòu)造函數(shù):
//在layout文件中 - 格式為: 包名.View名
<com.androiddemo.waveview.WaveView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
因?yàn)槲覀冞@個(gè)例子中是在layout中使用這個(gè)自定義View。所以我們當(dāng)前例子里面,只需要留下二個(gè)參數(shù)的public void WaveView(Context context, AttributeSet attrs) {}
構(gòu)造函數(shù)即可。
2.繪制內(nèi)容(onDraw)
onDraw是實(shí)際繪制的部分,也就是我們真正關(guān)心的部分,使用的是Canvas繪圖。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
我們是不是在想這個(gè)百度個(gè)人中心效果到底是怎么實(shí)現(xiàn)的,在這里我要貼個(gè)圖:
哈哈。沒(méi)錯(cuò)。那二個(gè)上下浮動(dòng)的曲線。我們可以用同時(shí)畫(huà)二個(gè)線,一個(gè)sin函數(shù),一個(gè)cos函數(shù)。而且處于同一水平線。不就一個(gè)交錯(cuò)的波浪了。
好,第一步的大概思路咱們有了。咱們?cè)偎伎既绾萎?huà)這些線呢。
這里先介紹幾個(gè)基本知識(shí)點(diǎn):
- 了解Path:
不清楚這個(gè)Path的一定要一定要一定要看下面這個(gè)Path基礎(chǔ)鏈接
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.
嗯,沒(méi)錯(cuò)依舊是拿來(lái)裝逼的,如果你看不懂的話,不用擔(dān)心,其實(shí)并沒(méi)有什么卵用。
通俗解釋:
Path封裝了由直線和曲線(二次,三次貝塞爾曲線)構(gòu)成的幾何路徑。你能用Canvas中的drawPath來(lái)把這條路徑畫(huà)出來(lái)(同樣支持Paint的不同繪制模式),也可以用于剪裁畫(huà)布和根據(jù)路徑繪制文字。我們有時(shí)會(huì)用Path來(lái)描述一個(gè)圖像的輪廓,所以也會(huì)稱為輪廓線(輪廓線僅是Path的一種使用方法,兩者并不等價(jià))
我就列舉出我們這次的仿百度效果會(huì)使用的幾個(gè)方法:
作用 | 相關(guān)方法 | 備注 |
---|---|---|
移動(dòng)起點(diǎn) | moveTo | 移動(dòng)下一次操作的起點(diǎn)位置 |
連接直線 | lineTo | 添加上一個(gè)點(diǎn)到當(dāng)前點(diǎn)之間的直線到Path |
重置路徑 | reset, rewind | 清除Path中的內(nèi)容reset不保留內(nèi)部數(shù)據(jù)結(jié)構(gòu),但會(huì)保留FillType.****rewind會(huì)保留內(nèi)部的數(shù)據(jù)結(jié)構(gòu),但不保留FillType |
沒(méi)錯(cuò)。為啥我列舉了這幾個(gè)方法呢。有人要問(wèn),lineTo不是畫(huà)直線的么。其實(shí)這個(gè)sin和cos曲線就是被我們一小段一小段的用線段畫(huà)出來(lái)的。
/**
* y=Asin(ωx+φ)+k
* A—振幅越大,波形在y軸上最大與最小值的差值越大
* ω—角速度, 控制正弦周期(單位角度內(nèi)震動(dòng)的次數(shù))
* φ—初相,反映在坐標(biāo)系上則為圖像的左右移動(dòng)。這里通過(guò)不斷改變?chǔ)?達(dá)到波浪移動(dòng)效果
* k—偏距,反映在坐標(biāo)系上則為圖像的上移或下移。
*/
比如畫(huà)上述這個(gè)sin函數(shù)。我們畫(huà)好后。怎么讓他不停的往左移動(dòng),產(chǎn)生波浪的效果呢。這時(shí)候就會(huì)想到重新繪制,然后再畫(huà)一遍,但是這時(shí)候不能原來(lái)這個(gè)sin函數(shù)。sin里面的φ參數(shù)要變一下,這樣再次重繪的時(shí)候。新畫(huà)出來(lái)的sin線就是一個(gè)被左右方向移動(dòng)后的線了。給你的感覺(jué)不就是像波浪一樣往右邊移動(dòng)了?。。?!
所以我們就知道了:(以sin為例)
- 畫(huà)出用lineTo在X軸上畫(huà)出一段段小的線段,拼成一個(gè)sin曲線圖
- 畫(huà)完這個(gè)曲線后重新執(zhí)行繪圖,這時(shí)候的改變sin函數(shù)內(nèi)部參數(shù),畫(huà)出來(lái)的曲線已經(jīng)在上一次的曲線的基礎(chǔ)上被左右移動(dòng)過(guò)了。
這下了解了這些。我們?cè)僮屑?xì)的分析下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軸上最大與最小值的差值越大
* ω—角速度, 控制正弦周期(單位角度內(nèi)震動(dòng)的次數(shù))
* φ—初相,反映在坐標(biāo)系上則為圖像的左右移動(dòng)。這里通過(guò)不斷改變?chǔ)?達(dá)到波浪移動(dòng)效果
* k—偏距,反映在坐標(biāo)系上則為圖像的上移或下移。
*/
y = (float) (8 * Math.cos(ω * x + φ) +8);
y2 = (float) (8 * Math.sin(ω * x + φ));
mAbovePath.lineTo(x, y);
mBelowWavePath.lineTo(x, y2);
//回調(diào) 把y坐標(biāo)的值傳出去(在activity里面接收讓小機(jī)器人隨波浪一起搖擺)
mWaveAnimationListener.OnWaveAnimation(y);
}
mAbovePath.lineTo(getRight(),getBottom());
mBelowWavePath.lineTo(getRight(),getBottom());
canvas.drawPath(mAbovePath,mAboveWavePaint);
canvas.drawPath(mBelowWavePath,mBelowWavePaint);
postInvalidateDelayed(20);
}
第一步:我們可以看到它是先通過(guò)for循環(huán)
for (float x = 0; x <= getWidth(); x += 20) {
}
把這個(gè)繪畫(huà)的曲線在X軸上分割成為一段段。每一段再用線段畫(huà)出來(lái)就可以了。
而每一段的畫(huà)又是要按照sin或者cos的函數(shù)來(lái)畫(huà)。并且是通過(guò)lineTo方法來(lái)。所以最后合在一起就是:
for (float x = 0; x <= getWidth(); x += 20) {
/**
* y=Asin(ωx+φ)+k
* A—振幅越大,波形在y軸上最大與最小值的差值越大
* ω—角速度, 控制正弦周期(單位角度內(nèi)震動(dòng)的次數(shù))
* φ—初相,反映在坐標(biāo)系上則為圖像的左右移動(dòng)。這里通過(guò)不斷改變?chǔ)?達(dá)到波浪移動(dòng)效果
* k—偏距,反映在坐標(biāo)系上則為圖像的上移或下移。
*/
y = (float) (8 * Math.cos(ω * x + φ) +8);
y2 = (float) (8 * Math.sin(ω * x + φ));
mAbovePath.lineTo(x, y);
mBelowWavePath.lineTo(x, y2);
}
這時(shí)候如果我們canvas.drawPath方法來(lái)畫(huà)出我們上面的這個(gè)處理過(guò)的path。就可以畫(huà)出來(lái)相應(yīng)的sin或者cos線了。
第二步:重新繪制曲線
在onDraw()方法的結(jié)尾處加上:
postInvalidateDelayed(20);
這個(gè)方法會(huì)再20毫秒后會(huì)重新調(diào)用onDraw()方法。
然后再onDraw()方法的開(kāi)始部分。我們要把path重新置空:
mAbovePath.reset(); mBelowWavePath.reset();
然后改變Asin(ωx+φ)+k的(φ—初相)這個(gè)值:
φ-=0.1f;
從而再一次畫(huà)出來(lái)的曲線就已經(jīng)左右被移動(dòng)過(guò)了。讓你產(chǎn)生波浪的感覺(jué)。
好的,我們已經(jīng)學(xué)完了那二個(gè)波浪的成(zhuang)功(B)實(shí)現(xiàn)了。如何來(lái)實(shí)現(xiàn)那個(gè)頭像跟隨著曲線一起動(dòng)呢。其實(shí)很簡(jiǎn)單。剛才我們能畫(huà)出曲線。是通過(guò)Path 的lineTo方法不斷的傳入相應(yīng)的(x,y)坐標(biāo),從而畫(huà)出一個(gè)個(gè)線段,從而拼成了曲線,那就是我們能拿到每個(gè)線段的Y軸坐標(biāo)上的值。也就是:
y = (float) (8 * Math.cos(ω * x + φ) +8);
y2 = (float) (8 * Math.sin(ω * x + φ));
那我們只要:
- 拿到圖片對(duì)象:
imageView = (ImageView) findViewById(R.id.image); - 把上面的曲線的y或者y1值拿過(guò)來(lái),比如我拿的是y。
- 讓imageView與它的父View之間的margin中的bottom屬性值等于這個(gè)y的值就可以了(demo里面是y+2)。這樣就不停的上下的浮動(dòng)了。
lp.setMargins(0,0,0,(int)y+2);
imageView.setLayoutParams(lp);
附上Activity及l(fā)ayout的代碼:
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="其他內(nèi)容"
android:textSize="24sp" />
</LinearLayout>
</LinearLayout>
最后咱們做出來(lái)的效果圖就是這樣滴:
最后再次感謝大神感謝CSDN的Zcoder2013
文章鏈接:http://blog.csdn.net/u011507982/article/details/53414422