程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> BezierDemo源碼解析

BezierDemo源碼解析

編輯:關於C++

這篇文章中我們比較了DraggableFlagView和BezierDemo兩個項目的區別,提到將對其中一個做源碼分析,那麼我們就來分析BezierDemo的源碼吧,因為這個項目的源碼最簡單,可以更直接的去分析核心的東西。但是效果還是DraggableFlagView好些。我盡量講的詳細些,滿足更多的初學者。這篇文章主要分析拉伸效果的實現。

源碼結構

BezierDemo只有兩個java文件

QQ圖片20150310211436.png

其中MainActivity.java是程序界面,而BezierView.java是實現了粘連拉伸效果的類。

MainActivity.java

package github.chenupt.bezier;

import android.app.Activity;
import android.os.Bundle;


public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }


}

activity_main.xml



    

這裡有個疑問:為啥BezierView控件的layout_width和layout_height為match_parent。 這是因為這個代碼很粗糙,哈哈。

 

好了,從上面的activity可以看出,所有的功能都是BezierView控件實現的,因此我們直接轉向BezierView.java

先貼代碼

package github.chenupt.bezier;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.AnimationDrawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;

/**
 * Created by [email protected] on 11/20/14.
 * Description : custom layout to draw bezier
 */
public class BezierView extends FrameLayout {

    // 默認定點圓半徑
    public static final float DEFAULT_RADIUS = 20;

    private Paint paint;
    private Path path;

    // 手勢坐標
    float x = 300;
    float y = 300;

    // 錨點坐標
    float anchorX = 200;
    float anchorY = 300;

    // 起點坐標
    float startX = 100;
    float startY = 100;

    // 定點圓半徑
    float radius = DEFAULT_RADIUS;

    // 判斷動畫是否開始
    boolean isAnimStart;
    // 判斷是否開始拖動
    boolean isTouch;

    ImageView exploredImageView;
    ImageView tipImageView;

    public BezierView(Context context) {
        super(context);
        init();
    }

    public BezierView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public BezierView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        path = new Path();

        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        paint.setStrokeWidth(2);
        paint.setColor(Color.RED);

        LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        exploredImageView = new ImageView(getContext());
        exploredImageView.setLayoutParams(params);
        exploredImageView.setImageResource(R.drawable.tip_anim);
        exploredImageView.setVisibility(View.INVISIBLE);

        tipImageView = new ImageView(getContext());
        tipImageView.setLayoutParams(params);
        tipImageView.setImageResource(R.drawable.skin_tips_newmessage_ninetynine);

        addView(tipImageView);
        addView(exploredImageView);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        exploredImageView.setX(startX - exploredImageView.getWidth()/2);
        exploredImageView.setY(startY - exploredImageView.getHeight()/2);

        tipImageView.setX(startX - tipImageView.getWidth()/2);
        tipImageView.setY(startY - tipImageView.getHeight()/2);

        super.onLayout(changed, left, top, right, bottom);
    }



    private void calculate(){
        float distance = (float) Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2));
        radius = -distance/15+DEFAULT_RADIUS;

        if(radius < 9){
            isAnimStart = true;

            exploredImageView.setVisibility(View.VISIBLE);
            exploredImageView.setImageResource(R.drawable.tip_anim);
            ((AnimationDrawable) exploredImageView.getDrawable()).stop();
            ((AnimationDrawable) exploredImageView.getDrawable()).start();

            tipImageView.setVisibility(View.GONE);
        }

        // 根據角度算出四邊形的四個點
        float offsetX = (float) (radius*Math.sin(Math.atan((y - startY) / (x - startX))));
        float offsetY = (float) (radius*Math.cos(Math.atan((y - startY) / (x - startX))));

        float x1 = startX - offsetX;
        float y1 = startY + offsetY;

        float x2 = x - offsetX;
        float y2 = y + offsetY;

        float x3 = x + offsetX;
        float y3 = y - offsetY;

        float x4 = startX + offsetX;
        float y4 = startY - offsetY;

        path.reset();
        path.moveTo(x1, y1);
        path.quadTo(anchorX, anchorY, x2, y2);
        path.lineTo(x3, y3);
        path.quadTo(anchorX, anchorY, x4, y4);
        path.lineTo(x1, y1);

        // 更改圖標的位置
        tipImageView.setX(x - tipImageView.getWidth()/2);
        tipImageView.setY(y - tipImageView.getHeight()/2);
    }

    @Override
    protected void onDraw(Canvas canvas){
        if(isAnimStart || !isTouch){
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
        }else{
            calculate();
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
            canvas.drawPath(path, paint);
            canvas.drawCircle(startX, startY, radius, paint);
            canvas.drawCircle(x, y, radius, paint);
        }
        super.onDraw(canvas);
    }



    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_DOWN){
            // 判斷觸摸點是否在tipImageView中
            Rect rect = new Rect();
            int[] location = new int[2];
            tipImageView.getDrawingRect(rect);
            tipImageView.getLocationOnScreen(location);
            rect.left = location[0];
            rect.top = location[1];
            rect.right = rect.right + location[0];
            rect.bottom = rect.bottom + location[1];
            if (rect.contains((int)event.getRawX(), (int)event.getRawY())){
                isTouch = true;
            }
        }else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL){
            isTouch = false;
            tipImageView.setX(startX - tipImageView.getWidth()/2);
            tipImageView.setY(startY - tipImageView.getHeight()/2);
        }
        invalidate();
        if(isAnimStart){
            return super.onTouchEvent(event);
        }
        anchorX =  (event.getX() + startX)/2;
        anchorY =  (event.getY() + startY)/2;
        x =  event.getX();
        y =  event.getY();
        return true;
    }


}

該控件是一個自定義的FrameLayout,之所以不用自定義view,是為了能直接添加顯示消息數目的圖片。

關於成員變量的那部分注釋已經比較清楚了,我直接看看

init()方法

在init方法中首先初始化了畫筆paint,這個paint就是繪制粘連拉伸效果的。然後paint初始化代碼下面為FrameLayout添加了兩個圖片:exploredImageView和tipImageView,exploredImageView是在拉斷之後顯示的氣泡,而tipImageView是數字提示,這兩個ImageView都只是為了輔助模仿qq,但不是我們要討論的核心。

onLayout()方法

非重點,略。

calculate()方法

這是根據手指拖動位置計算各坐標的的方法,同時還在這裡根據坐標點將path路徑也定義了:

        path.reset();
        path.moveTo(x1, y1);
        path.quadTo(anchorX, anchorY, x2, y2);
        path.lineTo(x3, y3);
        path.quadTo(anchorX, anchorY, x4, y4);
        path.lineTo(x1, y1);

這端代碼是粘連拉伸效果的核心。一會而我們做的各種實驗都是在這裡修修改改。

onDraw()方法

    @Override
    protected void onDraw(Canvas canvas){
        if(isAnimStart || !isTouch){
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
        }else{
            calculate();
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
            canvas.drawPath(path, paint);
           // canvas.drawCircle(startX, startY, radius, paint);
           // canvas.drawCircle(x, y, radius, paint);
        }
        super.onDraw(canvas);
    }

這個方法調用了上面的calculate方法,然後根據計算出的值繪制path和圓圈。

onTouchEvent()方法

這個方法將根據觸摸點的位置變化記錄必要的位置信息,供calculate()方法計算,同時在必要的地方發送繪制請求。

 

一步一步分解

如果講到這裡就結束,你肯定不滿意-“我還是沒明白貝塞爾曲線是如何應用到裡面的呢”。為了徹底明白我們將做幾個分解代碼的實驗。

 

首先我們找到onDraw方法,

    @Override
    protected void onDraw(Canvas canvas){
        if(isAnimStart || !isTouch){
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
        }else{
            calculate();
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
            canvas.drawPath(path, paint);
            canvas.drawCircle(startX, startY, radius, paint);
            canvas.drawCircle(x, y, radius, paint);
        }
        super.onDraw(canvas);
    }

if(isAnimStart || !isTouch){

中的代碼是拉斷之後的效果,不去管他。

主要看else中的代碼

首先調用了calculate()方法,然後調用了

 canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);

這個去掉也無所謂。

接著繪制了一條帶有貝塞爾曲線的封閉路徑:

 canvas.drawPath(path, paint);

然後分別繪制了兩端的圓圈。

 

為了更直觀的看出效果,我們將原本

// 默認定點圓半徑
public static final float DEFAULT_RADIUS = 20;

改成

// 默認定點圓半徑
public static final float DEFAULT_RADIUS = 150;

這樣大點會更清楚的看到拉伸過程,而且拉很長也不會斷,拉斷的臨界點是下面代碼決定的:

calculate方法中

float distance = (float) Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2));
radius = -distance/15+DEFAULT_RADIUS;

if(radius < 9){
isAnimStart = true;

更改之後得到的效果如下:

QQ圖片20150311010955.png

你看我都拉了半邊屏幕。

但是這樣仍然難以看到曲線是如何繪制的,這是因為畫筆paint的繪制類型是填充模式的,我們改成線條模式:

將init()方法改成

private void init(){
path = new Path();

paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
paint.setColor(Color.RED);

......

這樣我們就能看到線條是如何組合的了:

 

QQ圖片20150311011548.png

可以看出的確是兩個圓圈和一條封閉的路徑組成的。那個數字圖片有點礙眼,我們想辦法去掉

 

在calculate()方法的適當位置加上

 tipImageView.setVisibility(View.GONE);

我是加在第三行左右,總之能保證會被執行就行。我不敢說加在這裡最合適,我只是單純的想去掉它而已。

下面是去掉之後來回拉伸的變換圖:

654444.gif

有點猥瑣。。。。

現在我們將兩個圓圈也去掉吧,這兩個圓圈僅僅是根據兩點之間距離的大小改變了下半徑而已(第二個點也改變了圓點坐標)。貝塞爾曲線在中間那部分,讓我們看看包含了貝塞爾曲線的path路徑的真面目。

去掉圓圈只需將ondraw方法的相關代碼注釋掉:

QQ圖片20150311013309.png

下面是注釋之後的效果:

8487498.gif

這就是我們的path了。

回到構建這個path的代碼,在calculate方法中:

        path.reset();
        path.moveTo(x1, y1);
       
        path.quadTo(anchorX, anchorY, x2, y2);
        path.lineTo(x3, y3);
        path.quadTo(anchorX, anchorY, x4, y4);
     
        path.lineTo(x1, y1);

其中lineTo方法是繪制直線,quadTo方法就是繪制貝塞爾曲線,准確的說,是繪制二階貝塞爾曲線。為了能看出path的先後順序,我們分別定義

(x1, y1)為A點

(x2, y2)為B點

(x3, y3)為C點

(x4, y4)為D點

(anchorX, anchorY)為X點,這是二階貝塞爾曲線的控制點,這裡有兩條二階貝塞爾曲線,都是同一個控制點。

同時在canvas中將這幾個點的字母標注出來,具體的做法是調用canvas.drawText,修改具體的代碼我就不發了。

QQ圖片20150311113952.png

每個點的顯示位置有所偏差(尤其是X點),這是因為canvas.drawText的參數需要根據字符的大小做調整,我為了簡便,沒有去做,但是這些點你應該知道他們的實際位置,A,B,C,D很好辨認,但是X應該是在中間才對。

有了上面那幅圖對於這段代碼就好理解了

        path.moveTo(x1, y1);
       
        path.quadTo(anchorX, anchorY, x2, y2);
        path.lineTo(x3, y3);
        path.quadTo(anchorX, anchorY, x4, y4);
     
        path.lineTo(x1, y1);

拉伸的粘連效果主要取決於quadTo繪制的兩條貝塞爾曲線,這兩條曲線以他們之間的中間位置為控制點,導致曲線以相同的弧度往內彎曲。當兩端的圓圈距離越來越長,控制點的位置以及兩條曲線的端點也跟著變化(需要根據距離計算端點和控制點的位置)就形成了橡皮筋的粘連效果。

 

各坐標點的計算

那麼現在的最後一個問題是如何尋找這些變化的點。

首先我們需要記錄手指運動過程中,觸摸點的變化情況,在demo中是使用(x,y)來代表這個觸摸點,然後根據(startX,startY)(這個點是寫死的)計算出控制點的坐標(anchorX,anchorY)

代碼如下

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_DOWN){
            // 判斷觸摸點是否在tipImageView中
            Rect rect = new Rect();
            int[] location = new int[2];
            tipImageView.getDrawingRect(rect);
            tipImageView.getLocationOnScreen(location);
            rect.left = location[0];
            rect.top = location[1];
            rect.right = rect.right + location[0];
            rect.bottom = rect.bottom + location[1];
            if (rect.contains((int)event.getRawX(), (int)event.getRawY())){
                isTouch = true;
            }
        }else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL){
            isTouch = false;
            tipImageView.setX(startX - tipImageView.getWidth()/2);
            tipImageView.setY(startY - tipImageView.getHeight()/2);
        }
        invalidate();
        if(isAnimStart){
            return super.onTouchEvent(event);
        }
        anchorX =  (event.getX() + startX)/2;
        anchorY =  (event.getY() + startY)/2;
        x =  event.getX();
        y =  event.getY();
        return true;
    }

 

其中if和else代碼塊中的的代碼和粘連效果無關,這些代碼是關於氣泡的ImageView顯示與消失的。

主要就是下面的代碼

        invalidate();
        if(isAnimStart){
            return super.onTouchEvent(event);
        }
        anchorX =  (event.getX() + startX)/2;
        anchorY =  (event.getY() + startY)/2;
        x =  event.getX();
        y =  event.getY();

可以看出在onTouchEvent中,主要工作是記錄,坐標點的計算還是在calculate()方法裡(不過這裡也簡單的計算了控制點的坐標(anchorX,anchorY),其實這也可以放到calculate裡面)。另外

invalidate()方法我覺得還是放在最後比較好。不過沒什麼大礙,也就是落後一個點而已,你根本感覺不到。

 

而calculate()方法裡面對坐標的計算也很簡單,沒幾行代碼,結合上面的幾幅圖應該很容易解出來。這裡就不再贅述了。

 

 

其實整篇文章可以用一句話來概括:粘連效果的關鍵是由同一個控制點(中間點)“拖住”兩條貝塞爾曲線。

最後做一點補充,為了將橡皮的效果做的更逼真,這個demo中還動態的改變了兩端圓點的半徑,當然這也會導致其他點也做相應的改變

        float distance = (float) Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2));
        radius = -distance/15+DEFAULT_RADIUS;

 

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved