編輯:關於Android編程
從這篇開始,我將延續androidGraphics系列文章把圖片相關的知識給大家講完,這一篇先稍微進階一下,給大家把《Android Graphics(二):路徑及文字》略去的quadTo(二階貝塞爾)函數,給大家補充一下。
本篇最終將以兩個例子給大家演示貝塞爾曲線的強大用途:
1、手勢軌跡
利用貝塞爾曲線,我們能實現平滑的手勢軌跡效果
2、水波紋效果
電池充電時,有些手機會顯示水波紋效果,就是這樣做出來的。
廢話不多說,開整吧
一、概述
在《android Graphics(二):路徑及文字》中我們略去了有關所有貝賽爾曲線的知識,在Path中有四個函數與貝賽爾曲線有關:
//二階貝賽爾
publicvoidquadTo(floatx1,floaty1,floatx2,floaty2)
publicvoidrQuadTo(floatdx1,floatdy1,floatdx2,floatdy2)
//三階貝賽爾
publicvoidcubicTo(floatx1,floaty1,floatx2,floaty2,floatx3,floaty3)
publicvoidrCubicTo(floatx1,floaty1,floatx2,floaty2,floatx3,floaty3)
//二階貝賽爾
public void quadTo(float x1, float y1, float x2, float y2)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
//三階貝賽爾
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
這裡的四個函數的具體意義我們後面會具體詳細講解,我們這篇也就是利用這四個函數來實現我們的貝賽爾曲線相關的效果的。
1、貝賽爾曲線來源
在數學的數值分析領域中,貝賽爾曲線(Bézier曲線)是電腦圖形學中相當重要的參數曲線。更高維度的廣泛化貝塞爾曲線就稱作貝塞爾曲面,其中貝塞爾三角是一種特殊的實例。
貝塞爾曲線於1962年,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發表,他運用貝塞爾曲線來為汽車的主體進行設計。貝塞爾曲線最初由Paul de Casteljau於1959年運用de Casteljau算法開發,以穩定數值的方法求出貝塞爾曲線。
2、貝賽爾曲線公式
這部分是很有難度的,大家做好准備了哦
一階貝賽爾曲線
其公式可概括為:
對應動畫演示為:
P0為起點、P1為終點,t表示當前時間,B(t)表示公式的結果值。
注意,曲線的意義就是公式結果B(t)隨時間的變化,其取值所形成的軌跡。在動畫中,黑色點表示在當前時間t下公式B(t)的取值。而紅色的那條線就不在各個時間點下不同取值的B(t)所形成的軌跡。
總而言之:對於一階貝賽爾曲線,大家可以理解為在起始點和終點形成的這條直線上,勻速移動的點。
二階貝賽爾曲線
同樣,先來看看二階貝賽爾曲線的公式(雖然看不懂,呵呵)
大家也不用研究這個公式了,沒一定數學功底也研究不出來了啥,咱還是看動畫吧
在這裡P0是起始點,P2是終點,P1是控制點
假設將時間定在t=0.25的時刻,此時的狀態如下圖所示:
首先,P0點和P1點形成了一條貝賽爾曲線,還記得我們上面對一階貝賽爾曲線的總結麼:就是一個點在這條直線上做勻速運動;所以P0-P1這條直線上的移動的點就是Q0;
同樣,P1,P2形成了一條一階貝賽爾曲線,在這條一階貝賽爾曲線上,它們的隨時間移動的點是Q1
最後,動態點Q0和Q1又形成了一條一階貝賽爾曲線,在它們這條一階貝賽爾曲線動態移動的點是B
而B的移動軌跡就是這個二階貝賽爾曲線的最終形態。從上面的講解大家也可以知道,之所以叫它二階貝賽爾曲線是因為,B的移動軌跡是建立在兩個一階貝賽爾曲線的中間點Q0,Q1的基礎上的。
在理解了二階貝賽爾曲線的形成原理以後,我們就不難理解三階貝賽爾曲線了
三階貝賽爾曲線
同樣,先列下基本看不懂的公式
這玩意估計也看不懂,講了也沒什麼意義,還是結合動畫來吧
同樣,我們取其中一點來講解軌跡的形成原理,當t=0.25時,此時狀態如下:
同樣,P0是起始點,P3是終點;P1是第一個控制點,P2是第二個控制點;
首先,這裡有三條一階貝賽爾曲線,分別是P0-P1,P1-P2,P2-P3;
他們隨時間變化的點分別為Q0,Q1,Q2
然後是由Q0,Q1,Q2這三個點,再次連接,形成了兩條一階貝賽爾曲線,分別是Q0—Q1,Q1—Q2;他們隨時間變化的點為R0,R1
同樣,R0和R1同樣可以連接形成一條一階貝賽爾曲線,在R0—R1這條貝賽爾曲線上隨時間移動的點是B
而B的移動軌跡就是這個三階貝賽爾曲線的最終形狀。
從上面的解析大家可以看出,所謂幾階貝賽爾曲線,全部是由一條條一階貝賽爾曲線搭起來的;
在上圖中,形成一階貝賽爾曲線的直線是灰色的,形成二階貝賽爾曲線線是綠色的,形成三階貝賽爾曲線的線是藍色的。
在理解了上面的二階和三階貝賽爾曲線以後,我們再來看幾個貝賽爾曲線的動態圖
四階貝賽爾曲線
五階貝賽爾曲線
這裡就不再一一講解形成原理了,大家理解了二階和三階貝賽爾曲線以後,這兩條的看看就好了,想必大家也是能自己推出四階貝賽爾曲線的形成原理的。
3、貝賽爾曲線與PhotoShop鋼筆工具
如果有些同學不懂PhotoShop,這篇文章可能就會有些難度了,本篇文章主要是利用PhotoShop的鋼筆工具來得出具體貝塞爾圖像的
這麼屌的貝賽爾曲線,在專業繪圖工具PhotoShop中當然會有它的蹤影,它就是鋼筆工具,鋼筆工具所使用的路徑彎曲效果就是二階貝賽爾曲線。
我來給大家演示一下鋼筆工具的用法:
我們拿最終成形的圖形來看一下為什麼鋼筆工具是二階貝賽爾曲線:
右圖演示的假設某一點t=0.25時,動態點B的位置圖
同樣,這裡P0是起始點,P2是終點,P1是控制點;
P0-P1、P1-P2形成了第一層的一階貝賽爾曲線。它們隨時間的動態點分別是Q0,Q1
動態點Q0,Q1又形成了第二層的一階貝賽爾曲線,它們的動態點是B.而B的軌跡跟鋼筆工具的形狀是完全一樣的。所以鋼筆工具的拉伸效果是使用的二階貝賽爾曲線!
這個圖與上面二階貝賽爾曲線t=0.25時的曲線差不多,大家理解起來難度也不大。
這裡需要注意的是,我們在使用鋼筆工具時,拖動的是P5點。其實二階貝賽爾曲線的控制點是其對面的P1點,鋼筆工具這樣設計是當然是因為操作起來比較方便。
好了,對貝賽爾曲線的知識講了那麼多,下面開始實戰了,看在代碼中,貝賽爾曲線是怎麼來做的。
二、Android中貝賽爾曲線之quadTo
在開篇中,我們已經提到,在Path類中有四個方法與貝賽爾曲線相關,分別是:
[java] view plain copy
print?
//二階貝賽爾
publicvoidquadTo(floatx1,floaty1,floatx2,floaty2)
publicvoidrQuadTo(floatdx1,floatdy1,floatdx2,floatdy2)
//三階貝賽爾
publicvoidcubicTo(floatx1,floaty1,floatx2,floaty2,floatx3,floaty3)
publicvoidrCubicTo(floatx1,floaty1,floatx2,floaty2,floatx3,floaty3)
//二階貝賽爾
public void quadTo(float x1, float y1, float x2, float y2)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
//三階貝賽爾
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
在這四個函數中quadTo、rQuadTo是二階貝賽爾曲線,cubicTo、rCubicTo是三階貝賽爾曲線;我們這篇文章以二階貝賽爾曲線的quadTo、rQuadTo為主,三階貝賽爾曲線cubicTo、rCubicTo用的使用方法與二階貝賽爾曲線類似,用處也比較少,這篇就不再細講了。
1、quadTo使用原理
這部分我們先來看看quadTo函數的用法,其定義如下:
publicvoidquadTo(floatx1,floaty1,floatx2,floaty2)
public void quadTo(float x1, float y1, float x2, float y2)
參數中(x1,y1)是控制點坐標,(x2,y2)是終點坐標
大家可能會有一個疑問:有控制點和終點坐標,那起始點是多少呢?
整條線的起始點是通過Path.moveTo(x,y)來指定的,而如果我們連續調用quadTo(),前一個quadTo()的終點,就是下一個quadTo()函數的起點;如果初始沒有調用Path.moveTo(x,y)來指定起始點,則默認以控件左上角(0,0)為起始點;大家可能還是有點迷糊,下面我們就舉個例子來看看
我們利用quadTo()來畫下面的這條波浪線:
最關鍵的是如何來確定控制點的位置!前面講過,PhotoShop中的鋼筆工具是二階貝賽爾曲線,所以我們可以利用鋼筆工具來模擬畫出這條波浪線來輔助確定控制點的位置
下面我們來看看這個路徑軌跡中,控制點分別在哪個位置
我們先看P0-P2這條軌跡,P0是起點,假設位置坐標是(100,300),P2是終點,假充位置坐標是(300,300);在以P0為起始點,P2為終點這條二階貝賽爾曲線上,P1是控制點,很明顯P1大概在P0,P2中間的位置,所以它的X坐標應該是200,關於Y坐標,我們無法確定,但很明顯的是P1在P0,P2點的上方,也就是它的Y值比它們的小,所以根據鋼筆工具上面的位置,我們讓P1的比P0,P2的小100;所以P1的坐標是(200,200)
同理,不難求出在P2,P4這條二階貝賽爾曲線上,它們的控制點P3的坐標位置應該是(400,400);P3的X坐標是400是,因為P3點是P2,P4的中間點;與P3與P1距離P0-P2-P4這條直線的距離應該是相等的。P1距離P0-P2的值為100;P3距離P2-P4的距離也應該是100,這樣不難算出P3的坐標應該是(400,400);
下面開始是代碼部分了。
2、示例代碼
(1)、自定義View
我們知道在動畫繪圖時,會調用onDraw(Canvas canvas)函數,我們如果重寫了onDraw(Canvas canvas)函數,那麼我們利用canvas在上面畫了什麼,就會顯示什麼。所以我們自定義一個View
publicclassMyViewextendsView{
publicMyView(Contextcontext){
super(context);
}
publicMyView(Contextcontext,AttributeSetattrs){
super(context,attrs);
}
@Override
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
Paintpaint=newPaint();
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.GREEN);
Pathpath=newPath();
path.moveTo(100,300);
path.quadTo(200,200,300,300);
path.quadTo(400,400,500,300);
canvas.drawPath(path,paint);
}
}
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = new Paint(); paint.setStyle(Paint.Style.STROKE); paint.setColor(Color.GREEN); Path path = new Path(); path.moveTo(100,300); path.quadTo(200,200,300,300); path.quadTo(400,400,500,300); canvas.drawPath(path,paint); }
}
這裡最重要的就是在onDraw(Canvas canvas)中創建Path的過程,我們在上面已經提到,第一個起始點是需要調用path.moveTo(100,300)來指定的,之後後一個path.quadTo的起始點是以前一個path.quadTo的終點為起始點的。有關控制點的位置如何查找,我們上面已經利用鋼筆工具給大家講解了,這裡就不再細講。
所以,大家在自定義控件的時候,要多跟UED溝通,看他們是如何來實現這個效果的,如果是用的鋼筆工具,那我們也可以效仿使用二階貝賽爾曲線來實現。
2、使用MyView
在自定義控件以後,然後直接把它引入到主布局文件中即可(main.xml)
http://schemas.android.com/apk/res/android”
android:orientation=“vertical”
android:layout_width=“fill_parent”
android:layout_height=“fill_parent”>
android:layout_width=“match_parent”
android:layout_height=“match_parent”/>
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
android:layout_width="match_parent"
android:layout_height="match_parent"/>
由於直接做為控件顯示,所以MainActivity不需要額外的代碼即可顯示,MainActivity代碼如下:
publicclassMyActivityextendsActivity{
/*
Calledwhentheactivityisfirstcreated.
*/
@Override
publicvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}
public class MyActivity extends Activity {
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}
源碼在文章底部給出
通過這個例子希望大家知道兩點:
整條線的起始點是通過Path.moveTo(x,y)來指定的,如果初始沒有調用Path.moveTo(x,y)來指定起始點,則默認以控件左上角(0,0)為起始點;而如果我們連續調用quadTo(),前一個quadTo()的終點,就是下一個quadTo()函數的起點;
三、手指軌跡
要實現手指軌跡其實是非常簡單的,我們只需要在自定義中攔截OnTouchEvent,然後根據手指的移動軌跡來繪制Path即可。
要實現把手指的移動軌跡連接起來,最簡單的方法就是直接使用Path.lineTo()就能實現把各個點連接起來。
1、實現方式一:Path.lineTo(x,y)
我們先來看看效果圖:
(1)、自定義View——MyView
首先,我們自定義一個View,完整代碼如下:
publicclassMyViewextendsView{
privatePathmPath=newPath();
publicMyView(Contextcontext){
super(context);
}
publicMyView(Contextcontext,AttributeSetattrs){
super(context,attrs);
}
@Override
publicbooleanonTouchEvent(MotionEventevent){
switch(event.getAction()){
caseMotionEvent.ACTION_DOWN:{
mPath.moveTo(event.getX(),event.getY());
returntrue;
}
caseMotionEvent.ACTION_MOVE:
mPath.lineTo(event.getX(),event.getY());
postInvalidate();
break;
default:
break;
}
returnsuper.onTouchEvent(event);
}
@Override
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
Paintpaint=newPaint();
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(mPath,paint);
}
publicvoidreset(){
mPath.reset();
invalidate();
}
}
public class MyView extends View {
private Path mPath = new Path(); public MyView(Context context) { super(context); } public MyView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: { mPath.moveTo(event.getX(), event.getY()); return true; } case MotionEvent.ACTION_MOVE: mPath.lineTo(event.getX(), event.getY()); postInvalidate(); break; default: break; } return super.onTouchEvent(event); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = new Paint(); paint.setColor(Color.GREEN); paint.setStyle(Paint.Style.STROKE); canvas.drawPath(mPath,paint); } public void reset(){ mPath.reset(); invalidate(); }
}
最重要的位置就是在重寫onTouchEvent的位置:
publicbooleanonTouchEvent(MotionEventevent){
switch(event.getAction()){
caseMotionEvent.ACTION_DOWN:{
mPath.moveTo(event.getX(),event.getY());
returntrue;
}
caseMotionEvent.ACTION_MOVE:
mPath.lineTo(event.getX(),event.getY());
postInvalidate();
break;
default:
break;
}
returnsuper.onTouchEvent(event);
}
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN: {
mPath.moveTo(event.getX(), event.getY());
return true;
}
case MotionEvent.ACTION_MOVE:
mPath.lineTo(event.getX(), event.getY());
postInvalidate();
break;
default:
break;
}
return super.onTouchEvent(event);
}
當用戶點擊屏幕的時候,我們調用mPath.moveTo(event.getX(), event.getY());然後在用戶移動手指時使用mPath.lineTo(event.getX(), event.getY());將各個點串起來。然後調用postInvalidate()重繪;
Path.moveTo()和Path.lineTo()的用法,大家如果看了《android Graphics(二):路徑及文字》之後,理解起來應該沒什麼難度,但這裡有兩個地方需要注意
第一:有關在case MotionEvent.ACTION_DOWN時return true的問題:return true表示當前控件已經消費了下按動作,之後的ACTION_MOVE、ACTION_UP動作也會繼續傳遞到當前控件中;如果我們在case MotionEvent.ACTION_DOWN時return false,那麼後序的ACTION_MOVE、ACTION_UP動作就不會再傳到這個控件來了。有關動作攔截的知識,後續會在這個系列中單獨來講,大家先期待下吧。
第二:這裡重繪控件使用的是postInvalidate();而我們以前也有用Invalidate()函數的。這兩個函數的作用都是用來重繪控件的,但區別是Invalidate()一定要在UI線程執行,如果不是在UI線程就會報錯。而postInvalidate()則沒有那麼多講究,它可以在任何線程中執行,而不必一定要是主線程。其實在postInvalidate()就是利用handler給主線程發送刷新界面的消息來實現的,所以它是可以在任何線程中執行,而不會出錯。而正是因為它是通過發消息來實現的,所以它的界面刷新可能沒有直接調Invalidate()刷的那麼快。
所以在我們確定當前線程是主線程的情況下,還是以invalide()函數為主。當我們不確定當前要刷新頁面的位置所處的線程是不是主線程的時候,還是用postInvalidate為好;
這裡我是故意用的postInvalidate(),因為onTouchEvent()本來就是在主線程中的,使用Invalidate()是更合適的。當我們
有關OnDraw函數就沒什麼好講的,就是把path給畫出來:
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
Paintpaint=newPaint();
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(mPath,paint);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(mPath,paint);
}
最後,我還額外寫了一個重置函數:
publicvoidreset(){
mPath.reset();
invalidate();
}
public void reset(){
mPath.reset();
invalidate();
}
(2)、主布局
然後看看布局文件(mian.xml)
http://schemas.android.com/apk/res/android”
android:orientation=“vertical”
android:layout_width=“fill_parent”
android:layout_height=“fill_parent”>
android:id=“@+id/reset”
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:text=“reset”/>
android:id=“@+id/myview”
android:layout_width=“match_parent”
android:layout_height=“match_parent”/>
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"> android:id="@+id/reset" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="reset"/>
android:id="@+id/myview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
沒什麼難度,就是把自定義控件添加到布局中
(3)、MyActivity
然後看MyActivity的操作:
publicclassMyActivityextendsActivity{
@Override
publicvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
finalMyViewmyView=(MyView)findViewById(R.id.myview);
findViewById(R.id.reset).setOnClickListener(newView.OnClickListener(){
@Override
publicvoidonClick(Viewv){
myView.reset();
}
});
}
}
public class MyActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
final MyView myView = (MyView)findViewById(R.id.myview); findViewById(R.id.reset).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { myView.reset(); } }); }
}
這裡實現的就是當點擊按鈕時,調用 myView.reset()來重置畫布;
源碼在文章底部給出
(4)、使用Path.lineTo()所存在問題
上面我們雖然實現了,畫出手指的移動軌跡,但我們仔細來看看畫出來的圖:
我們把S放大,明顯看出,在兩個點連接處有明顯的轉折,而且在S頂部位置橫縱坐標變化比較快的位置,看起來跟圖片這大後的馬賽克一樣;利用Path繪圖,是不可能出現馬賽克的,因為除了Bitmap以外的任何canvas繪圖全部都是矢量圖,也就是利用數學公式來作出來的圖,無論放在多大屏幕上,都不可能會出現馬賽克!這裡利用Path繪圖,在S頂部之所以看起來像是馬賽克是因為這個S是由各個不同點之間連線寫出來的,而之間並沒有平滑過渡,所以當坐標變化比較劇烈時,線與線之間的轉折就顯得特別明顯了。
所以要想優化這種效果,就得實現線與線之間的平滑過渡,很顯然,二階貝賽爾曲線就是干這個事的。下面我們就利用我們新學的Path.quadTo函數來重新實現下移動軌跡效果。
2、實現方式二(優化):使用Path.quadTo()函數實現過渡
(1)、原理概述
我們上面講了,使用Path.lineTo()的最大問題就是線段轉折處不夠平滑。Path.quadTo()可以實現平滑過渡,但使用Path.quadTo()的最大問題是,如何找到起始點和結束點。
下圖中,有用綠點表示的三個點,連成的兩條直線,很明顯他們轉折處是有明顯折痕的
下面我們在PhotoShop中利用鋼筆工具,看如何才能實現這兩條線之間的轉折
從這兩個線段中可以看出,我們使用Path.lineTo()的時候,是直接把手指觸點A,B,C給連起來。
而鋼筆工具要實現這三個點間的流暢過渡,就只能將這兩個線段的中間點做為起始點和結束點,而將手指的倒數第二個觸點B做為控制點。
大家可能會覺得,那這樣,在結束的時候,A到P0和P1到C1的這段距離豈不是沒畫進去?是的,如果Path最終沒有close的話,這兩段距離是被拋棄掉的。因為手指間滑動時,每兩個點間的距離很小,所以P1到C之間的距離可以忽略不計。
下面我們就利用這種方法在photoshop中求證,在連接多個線段時,是否能行?
在這個圖形中,有很多點連成了彎彎曲曲的線段,我們利用上面我們講的,將兩個線段的中間做為二階貝爾賽曲線的起始點和終點,把上一個手指的位置做為控制點,來看看是否真的能組成平滑的連線
整個連接過程如動畫所示:
在最終的路徑中看來,各個點間的連線是非常平滑的。從這裡也可以看出,在為了實現平滑效果,我們只能把開頭的線段一半和結束的線段的一半拋棄掉。
在講了原理之後,下面就來看看在代碼中如何來實現吧。
(2)、自定義View
先貼出完整代碼然後再細講:
publicclassMyViewextendsView{
privatePathmPath=newPath();
privatefloatmPreX,mPreY;
publicMyView(Contextcontext){
super(context);
}
publicMyView(Contextcontext,AttributeSetattrs){
super(context,attrs);
}
@Override
publicbooleanonTouchEvent(MotionEventevent){
switch(event.getAction()){
caseMotionEvent.ACTION_DOWN:{
mPath.moveTo(event.getX(),event.getY());
mPreX=event.getX();
mPreY=event.getY();
returntrue;
}
caseMotionEvent.ACTION_MOVE:{
floatendX=(mPreX+event.getX())/2;
floatendY=(mPreY+event.getY())/2;
mPath.quadTo(mPreX,mPreY,endX,endY);
mPreX=event.getX();
mPreY=event.getY();
invalidate();
}
break;
default:
break;
}
returnsuper.onTouchEvent(event);
}
@Override
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
Paintpaint=newPaint();
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.GREEN);
paint.setStrokeWidth(2);
canvas.drawPath(mPath,paint);
}
publicvoidreset(){
mPath.reset();
postInvalidate();
}
}
public class MyView extends View {
private Path mPath = new Path();
private float mPreX,mPreY;
public MyView(Context context) { super(context); } public MyView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN:{ mPath.moveTo(event.getX(),event.getY()); mPreX = event.getX(); mPreY = event.getY(); return true; } case MotionEvent.ACTION_MOVE:{ float endX = (mPreX+event.getX())/2; float endY = (mPreY+event.getY())/2; mPath.quadTo(mPreX,mPreY,endX,endY); mPreX = event.getX(); mPreY =event.getY(); invalidate(); } break; default: break; } return super.onTouchEvent(event); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = new Paint(); paint.setStyle(Paint.Style.STROKE); paint.setColor(Color.GREEN); paint.setStrokeWidth(2); canvas.drawPath(mPath,paint); } public void reset(){ mPath.reset(); postInvalidate(); }
}
最難的部分依然是onTouchEvent函數這裡:
publicbooleanonTouchEvent(MotionEventevent){
switch(event.getAction()){
caseMotionEvent.ACTION_DOWN:{
mPath.moveTo(event.getX(),event.getY());
mPreX=event.getX();
mPreY=event.getY();
returntrue;
}
…………
}
returnsuper.onTouchEvent(event);
}
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:{
mPath.moveTo(event.getX(),event.getY());
mPreX = event.getX();
mPreY = event.getY();
return true;
}
…………
}
return super.onTouchEvent(event);
}
在ACTION_DOWN的時候,利用 mPath.moveTo(event.getX(),event.getY())將Path的初始位置設置到手指的觸點處,如果不調用mPath.moveTo的話,會默認是從(0,0)開始的。然後我們定義兩個變量mPreX,mPreY來表示手指的前一個點。我們通過上面的分析知道,這個點是用來做控制點的。最後return true讓ACTION_MOVE,ACTION_UP事件繼續向這個控件傳遞。
在ACTION_MOVE時:
caseMotionEvent.ACTION_MOVE:{
floatendX=(mPreX+event.getX())/2;
floatendY=(mPreY+event.getY())/2;
mPath.quadTo(mPreX,mPreY,endX,endY);
mPreX=event.getX();
mPreY=event.getY();
invalidate();
}
case MotionEvent.ACTION_MOVE:{
float endX = (mPreX+event.getX())/2; float endY = (mPreY+event.getY())/2; mPath.quadTo(mPreX,mPreY,endX,endY); mPreX = event.getX(); mPreY =event.getY(); invalidate();
}
我們先找到結束點,我們說了結束點是這個線段的中間位置,所以很容易求出它的坐標endX,endY;控制點是上一個手指位置即mPreX,mPreY;那有些同學可能會問了,那起始點是哪啊。在開篇講quadTo()函數時,就已經說過,第一個起始點是Path.moveTo(x,y)定義的,其它部分,一個quadTo的終點,是下一個quadTo的起始點。
所以這裡的起始點,就是上一個線段的中間點。所以,這樣就與鋼筆工具繪制過程完全對上了:把各個線段的中間點做為起始點和終點,把終點前一個手指位置做為控制點。
後面的onDraw()和reset()函數就沒什麼難度了,上面的例子中也講過了,就不再贅述了
最終的效果圖如下:
同樣把lineTo和quadTo實現的S拿來對比下:
從效果圖中可以明顯可以看出,通過quadTo實現的曲線更順滑
源碼在文章底部給出
Ok啦,quadeTo的用法,到這裡就結束了,下部分再來講講rQuadTo的用法及波浪動畫效果
四、Path.rQuadTo()
1、概述
該函數聲明如下
publicvoidrQuadTo(floatdx1,floatdy1,floatdx2,floatdy2)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
其中:
dx1:控制點X坐標,表示相對上一個終點X坐標的位移坐標,可為負值,正值表示相加,負值表示相減;dy1:控制點Y坐標,相對上一個終點Y坐標的位移坐標。同樣可為負值,正值表示相加,負值表示相減;dx2:終點X坐標,同樣是一個相對坐標,相對上一個終點X坐標的位移值,可為負值,正值表示相加,負值表示相減;dy2:終點Y坐標,同樣是一個相對,相對上一個終點Y坐標的位移值。可為負值,正值表示相加,負值表示相減;
這四個參數都是傳遞的都是相對值,相對上一個終點的位移值。
比如,我們上一個終點坐標是(300,400)那麼利用rQuadTo(100,-100,200,100);
得到的控制點坐標是(300+100,400-100)即(500,300)
同樣,得到的終點坐標是(300+200,400+100)即(500,500)
所以下面這兩段代碼是等價的:
利用quadTo定義絕對坐標
path.moveTo(300,400);
path.quadTo(500,300,500,500);
path.moveTo(300,400);
path.quadTo(500,300,500,500);
與利用rQuadTo定義相對坐標
path.moveTo(300,400);
path.rQuadTo(100,-100,200,100)
path.moveTo(300,400);
path.rQuadTo(100,-100,200,100)
2、使用rQuadTo實現波浪線
在上篇中,我們使用quadTo實現了一個簡單的波浪線:
各個點具體計算過程,在上篇已經計算過了,下面是上篇中onDraw的代碼:
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
Paintpaint=newPaint();
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.GREEN);
Pathpath=newPath();
path.moveTo(100,300);
path.quadTo(200,200,300,300);
path.quadTo(400,400,500,300);
canvas.drawPath(path,paint);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint(); paint.setStyle(Paint.Style.STROKE); paint.setColor(Color.GREEN); Path path = new Path(); path.moveTo(100,300); path.quadTo(200,200,300,300); path.quadTo(400,400,500,300); canvas.drawPath(path,paint);
}
下面我們將它轉化為rQuadTo來重新實現下:
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
Paintpaint=newPaint();
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.GREEN);
Pathpath=newPath();
path.moveTo(100,300);
path.rQuadTo(100,-100,200,0);
path.rQuadTo(100,100,200,0);
canvas.drawPath(path,paint);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint(); paint.setStyle(Paint.Style.STROKE); paint.setColor(Color.GREEN); Path path = new Path(); path.moveTo(100,300); path.rQuadTo(100,-100,200,0); path.rQuadTo(100,100,200,0); canvas.drawPath(path,paint);
}
簡單來講,就是將原來的:
[java]view plaincopy
print?
path.moveTo(100,300);
path.quadTo(200,200,300,300);
path.quadTo(400,400,500,300);
path.moveTo(100,300);
path.quadTo(200,200,300,300);
path.quadTo(400,400,500,300);
轉化為:
path.moveTo(100,300);
path.rQuadTo(100,-100,200,0);
path.rQuadTo(100,100,200,0);
path.moveTo(100,300);
path.rQuadTo(100,-100,200,0);
path.rQuadTo(100,100,200,0);
第一句:path.rQuadTo(100,-100,200,0);是建立在(100,300)這個點基礎上來計算相對坐標的。
所以
控制點X坐標=上一個終點X坐標+控制點X位移 = 100+100=200;
控制點Y坐標=上一個終點Y坐標+控制點Y位移 = 300-100=200;
終點X坐標 = 上一個終點X坐標+終點X位移 = 100+200=300;
終點Y坐標 = 上一個終點Y坐標+控制點Y位移 = 300+0=300;
所以這句與path.quadTo(200,200,300,300);對等的
第二句:path.rQuadTo(100,100,200,0);是建立在它的前一個終點即(300,300)的基礎上來計算相對坐標的!
所以
控制點X坐標=上一個終點X坐標+控制點X位移 = 300+100=200;
控制點Y坐標=上一個終點Y坐標+控制點Y位移 = 300+100=200;
終點X坐標 = 上一個終點X坐標+終點X位移 = 300+200=500;
終點Y坐標 = 上一個終點Y坐標+控制點Y位移 = 300+0=300;
所以這句與path.quadTo(400,400,500,300);對等的
最終效果也是一樣的。
通過這個例子,只想讓大家明白一點:rQuadTo(float dx1, float dy1, float dx2, float dy2)中的位移坐標,都是以上一個終點位置為基准來做偏移的!
五、實現波浪效果
本節完成之後,將實現文章開頭的波浪效果,如下。
1、實現全屏波紋
上面我們已經能夠實現一個波形,只要我們再多實現幾個波形,就可以覆蓋整個屏幕了。
對應代碼如下:
publicclassMyViewextendsView{
privatePaintmPaint;
privatePathmPath;
privateintmItemWaveLength=400;
publicMyView(Contextcontext,AttributeSetattrs){
super(context,attrs);
mPath=newPath();
mPaint=newPaint();
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
mPath.reset();
intoriginY=300;
inthalfWaveLen=mItemWaveLength/2;
mPath.moveTo(-mItemWaveLength,originY);
for(inti=-mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
mPath.rQuadTo(halfWaveLen/2,-50,halfWaveLen,0);
mPath.rQuadTo(halfWaveLen/2,50,halfWaveLen,0);
}
canvas.drawPath(mPath,mPaint);
}
}
public class MyView extends View {
private Paint mPaint;
private Path mPath;
private int mItemWaveLength = 400;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
mPath = new Path();
mPaint = new Paint();
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPath.reset(); int originY = 300; int halfWaveLen = mItemWaveLength/2; mPath.moveTo(-mItemWaveLength,originY); for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){ mPath.rQuadTo(halfWaveLen/2,-50,halfWaveLen,0); mPath.rQuadTo(halfWaveLen/2,50,halfWaveLen,0); } canvas.drawPath(mPath,mPaint); }
}
最難的部分依然是在onDraw函數中:
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
mPath.reset();
intoriginY=300;
inthalfWaveLen=mItemWaveLength/2;
mPath.moveTo(-mItemWaveLength,originY);
for(inti=-mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
}
canvas.drawPath(mPath,mPaint);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
int originY = 300;
int halfWaveLen = mItemWaveLength/2;
mPath.moveTo(-mItemWaveLength,originY);
for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
}
canvas.drawPath(mPath,mPaint);
}
我們將mPath的起始位置向左移一個波長:
mPath.moveTo(-mItemWaveLength,originY);
mPath.moveTo(-mItemWaveLength,originY);
然後利用for循環畫出當前屏幕中可能容得下的所有波:
for(inti=-mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
}
for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
}
mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);畫的是一個波長中的前半個波,mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);畫的是一個波長中的後半個波。大家在這裡可以看到,屏幕左右都多畫了一個波長的圖形。這是為了波形移動做准備的。
到這裡,我們是已經能畫出來一整屏幕的波形了,下面我們把整體波形閉合起來
其中,圖中紅色區域是我標出來利用lineTo閉合的區域
publicclassMyViewextendsView{
privatePaintmPaint;
privatePathmPath;
privateintmItemWaveLength=400;
publicMyView(Contextcontext,AttributeSetattrs){
super(context,attrs);
mPath=newPath();
mPaint=newPaint();
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
@Override
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
mPath.reset();
intoriginY=300;
inthalfWaveLen=mItemWaveLength/2;
mPath.moveTo(-mItemWaveLength+dx,originY);
for(inti=-mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
}
mPath.lineTo(getWidth(),getHeight());
mPath.lineTo(0,getHeight());
mPath.close();
canvas.drawPath(mPath,mPaint);
}
}
public class MyView extends View {
private Paint mPaint;
private Path mPath;
private int mItemWaveLength = 400;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
mPath = new Path();
mPaint = new Paint();
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPath.reset(); int originY = 300; int halfWaveLen = mItemWaveLength/2; mPath.moveTo(-mItemWaveLength+dx,originY); for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){ mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0); mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0); } mPath.lineTo(getWidth(),getHeight()); mPath.lineTo(0,getHeight()); mPath.close(); canvas.drawPath(mPath,mPaint); }
}
這段代碼相比上面的代碼,增加了兩部分內容:
第一,將paint設置為填充:mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
第二,將path閉合:
mPath.moveTo(-mItemWaveLength+dx,originY);
for(inti=-mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
}
mPath.lineTo(getWidth(),getHeight());
mPath.lineTo(0,getHeight());
mPath.close();
mPath.moveTo(-mItemWaveLength+dx,originY);
for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
}
mPath.lineTo(getWidth(),getHeight());
mPath.lineTo(0,getHeight());
mPath.close();
2、實現移動動畫
讓波紋動起來其實挺簡單,利用調用在path.moveTo的時候,將起始點向右移動即可實現移動,而且只要我們移動一個波長的長度,波紋就會重合,就可以實現無限循環了。
為此我們定義一個動畫:
publicvoidstartAnim(){
ValueAnimatoranimator=ValueAnimator.ofInt(0,mItemWaveLength);
animator.setDuration(2000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(newLinearInterpolator());
animator.addUpdateListener(newValueAnimator.AnimatorUpdateListener(){
@Override
publicvoidonAnimationUpdate(ValueAnimatoranimation){
dx=(int)animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
public void startAnim(){
ValueAnimator animator = ValueAnimator.ofInt(0,mItemWaveLength);
animator.setDuration(2000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
dx = (int)animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
動畫的長度為一個波長,將當前值保存在類的成員變量dx中;
然後在畫圖的時候,在path.moveTo()中加上現在的移動值dx:mPath.moveTo(-mItemWaveLength+dx,originY);
完整的繪圖代碼如下:
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
mPath.reset();
intoriginY=300;
inthalfWaveLen=mItemWaveLength/2;
mPath.moveTo(-mItemWaveLength+dx,originY);
for(inti=-mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
}
mPath.lineTo(getWidth(),getHeight());
mPath.lineTo(0,getHeight());
mPath.close();
canvas.drawPath(mPath,mPaint);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
int originY = 300;
int halfWaveLen = mItemWaveLength/2;
mPath.moveTo(-mItemWaveLength+dx,originY);
for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
}
mPath.lineTo(getWidth(),getHeight());
mPath.lineTo(0,getHeight());
mPath.close();
canvas.drawPath(mPath,mPaint);
}
完整的MyView代碼如下:
[java]view plaincopy
print?
publicclassMyViewextendsView{
privatePaintmPaint;
privatePathmPath;
privateintmItemWaveLength=400;
privateintdx;
publicMyView(Contextcontext,AttributeSetattrs){
super(context,attrs);
mPath=newPath();
mPaint=newPaint();
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
@Override
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
mPath.reset();
intoriginY=300;
inthalfWaveLen=mItemWaveLength/2;
mPath.moveTo(-mItemWaveLength+dx,originY);
for(inti=-mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
}
mPath.lineTo(getWidth(),getHeight());
mPath.lineTo(0,getHeight());
mPath.close();
canvas.drawPath(mPath,mPaint);
}
publicvoidstartAnim(){
ValueAnimatoranimator=ValueAnimator.ofInt(0,mItemWaveLength);
animator.setDuration(2000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(newLinearInterpolator());
animator.addUpdateListener(newValueAnimator.AnimatorUpdateListener(){
@Override
publicvoidonAnimationUpdate(ValueAnimatoranimation){
dx=(int)animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
}
public class MyView extends View {
private Paint mPaint;
private Path mPath;
private int mItemWaveLength = 400;
private int dx;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
mPath = new Path();
mPaint = new Paint();
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPath.reset(); int originY = 300; int halfWaveLen = mItemWaveLength/2; mPath.moveTo(-mItemWaveLength+dx,originY); for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){ mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0); mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0); } mPath.lineTo(getWidth(),getHeight()); mPath.lineTo(0,getHeight()); mPath.close(); canvas.drawPath(mPath,mPaint); } public void startAnim(){ ValueAnimator animator = ValueAnimator.ofInt(0,mItemWaveLength); animator.setDuration(2000); animator.setRepeatCount(ValueAnimator.INFINITE); animator.setInterpolator(new LinearInterpolator()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { dx = (int)animation.getAnimatedValue(); postInvalidate(); } }); animator.start(); }
}
然後在MyActivity中開始動畫:
publicclassMyActivityextendsActivity{
/*
Calledwhentheactivityisfirstcreated.
*/
@Override
publicvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
finalMyViewmyView=(MyView)findViewById(R.id.myview);
myView.startAnim();
}
}
public class MyActivity extends Activity {
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
final MyView myView = (MyView)findViewById(R.id.myview);
myView.startAnim();
}
}
這樣就實現了動畫:
如果把波長設置為1000,就可以實現本段開篇的動畫了。
如果想讓波紋像開篇時那要同時向下移動,大家只需要在path.moveTo(x,y)的時候,通過動畫同時移動y坐標就可以了,代碼比較簡單,而且本文實在是太長了,具體實現就不再講了,大家可以在源碼中加以嘗試。
最近公司培訓新同事,我負責整理一點關於android的基礎知識,遙想當年,剛接觸android,也是一頭霧水,啥都不懂,就是靠看文檔和視頻,對andro
推薦閱讀:使用RecyclerView添加Header和Footer的方法
Android 調用WCF實例1. 構建服務端程序using System.ServiceModel;namespace yournamespace{ [Service
本文實現了Android程序文字翻轉動畫的小程序,具體代碼如下:先上效果圖如下:要求:沿Y軸正方向看,數值減1時動畫逆時針旋轉,數值加1時動畫順時針旋轉。實現動畫的具體細