Android 貝茲路徑實現QQ拖拽清除效果
純屬好奇心驅動寫的一個學習性Demo,效果如下:
vc23tNa1xNK7zPWyu7nm1PK1xFBhdGijrLb41eK49tbQvOTP8sTazeTH+rXE0Ke5+9X9ysfSu8z1sbTI+7b7x/rP36Os1tC85NXiuPZQYXRoysfTycG9zPWxtMj7tvvH+s/fus3Bvcz11rHP39fps8mho7+0z8LNvKO6PGJyIC8+DQo8aW1nIGFsdD0="這裡寫圖片描述" src="http://www.bkjia.com/uploads/allimg/160407/0413013240-1.png" title="\" />
兩個帶圓弧的線就是由三點確認的一個貝茲路徑:
在Android已經有提供畫貝茲路徑的介面,三個點傳進去,效果就出來了。
貝茲路徑是用三個或多個點來確定的一條曲線,它在圖形映像學中有相當重要的地位,Path中也提供了一些方法來給我們類比低階貝賽爾曲線。
本文的主要目的是學習貝茲路徑,看一下貝茲路徑的一下使用案例。
案例一:
案例二:
案例三:
案例四:
其實還有很多,還有QQ就是上面拖拽清除訊息的氣泡。看了之後你會好奇是怎麼做的吧。
這裡不講背景,只講貝茲路徑的一些效果,先從維基百科上盜圖,看看不同數量的點上繪製貝茲路徑的效果。
兩個點,繪製出來就是直線,也就是所謂的一階貝茲路徑
三個點,二階貝茲路徑:
四個點,三階貝茲路徑:
五個點,四階貝茲路徑:
六個點,五階貝茲路徑:
大家看到上面的圖,其實可以這麼理解,這也是參考網友的理解。不管是一階、二階、還是六階,他們都至少有兩個點,也就是起點和終點。先用橡皮筋把這兩個點串連好。而除了起點和終點之外的點都相當於對橡皮筋有吸引力的點,他們因為距離原因的不同,對橡皮筋產生不同程度的吸力,最終達到一個平衡狀態,所以橡皮筋往非起始點有所位移,如:
在Android中提供了繪製一階、二階、三階的介面:
一階介面:
public void lineTo(float x, float y)
二階介面:
public void quadTo(float x1, float y1, float x2, float y2)
三階介面:
public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
關於最上面的QQ拖拽清除效果,就是用的二階繪製的。
下面來簡要講解一下它的實現原理吧:
第一:那個99+訊息是一個ImageView,它是根據OnTouch事件移動的,這個好理解。這是是通過在onTouch方法裡面不斷的setX()、setY()實現的。
第二:記錄起始位置p1(x1,y1),記錄手指拖拽位置p2(x2,y2),有了這兩點我們就可以繪製直線了,但是我們這裡繪製的是帶有貝茲路徑的Path,所以重要工作是構造Path
第三:構造Path,如:
知道了兩個黑點的座標,並且知道綠色線的寬度Len,很好求出P1、P2、P3當中的點了,畫個座標系,用三角函數就求出來了。當然這個寬度Len是根據兩個黑點之間的距離動態去計算了,它與兩個黑點之間的距離成反比。構造Path的步驟是:
p1-p3-p2之間:貝茲路徑
p2-p5之間:直線
p5-p3-p4之間:貝茲路徑
p4-p1之間:直線
構造了這樣一個Path,然後用實心的畫筆就畫出效果來了。
當然,要完全達到要求還要加上邏輯控制了,代碼如下:
package rander.com.bezier.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.os.Handler;import android.os.Looper;import android.util.AttributeSet;import android.util.TypedValue;import android.view.MotionEvent;import android.view.View;import android.widget.FrameLayout;import android.widget.ImageView;import android.widget.Toast;import rander.com.bezier.R;/** * Created by Rander.C on 16-4-3. */public class QQDragtoClearView extends FrameLayout { /** * 最大拖拽長度 */ private final int DRAG_MAX_LEN = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics()); /** * 預設的紅點半徑大小 */ private final int DEFAULT_RADIUS = 40; /** * 訊息數量背景,QQ裡面起始用的就是一個圖片,而不是在紅色背景上畫一個數量 */ private ImageView mIvMsgCountBg; /** * 拖拽清除的時候的動畫視圖 */ private ImageView mIvClearAnim; /** * 手觸摸的x,y座標 */ private float mTouchX; private float mTouchY; /** * 初始的x,y座標,預設安放的位置 */ private float mStartX = 300; private float mStartY = 300; /** * 手觸摸的座標和初始點座標的中間位置 * mCenterX = (mTouchX + mStartX)/2 * mCenterY = (mTouchY + mStartY )/2 * 這個是用來畫貝茲路徑的一個中轉點 */ private float mCenterX; private float mCenterY; private static final int TOUCH_SLOP = 10; /** * 畫貝茲路徑的畫筆 */ private Paint mPaint; /** * 畫貝茲路徑的Path */ private Path mPath; /** * 是否被點中了 */ private boolean isTouch; /** * 預設的半徑大小 */ private int mRaduis = DEFAULT_RADIUS; /** * 判斷貝茲路徑是否斷掉了 */ private boolean isBroken = false; public QQDragtoClearView(Context context) { this(context, null); } public QQDragtoClearView(Context context, AttributeSet attrs) { this(context, attrs, -1); } public QQDragtoClearView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /** * 初始化 */ private void init() { mPath = new Path(); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL_AND_STROKE); mPaint.setStrokeWidth(2); mPaint.setColor(Color.RED); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(60, 60); mIvClearAnim = new ImageView(getContext()); mIvClearAnim.setLayoutParams(params); mIvClearAnim.setImageResource(R.drawable.tip_anim); mIvClearAnim.setVisibility(INVISIBLE); mIvMsgCountBg = new ImageView(getContext()); mIvMsgCountBg.setLayoutParams(params); mIvMsgCountBg.setImageResource(R.drawable.skin_tips_newmessage_ninetynine); mIvMsgCountBg.setVisibility(VISIBLE); addView(mIvClearAnim); addView(mIvMsgCountBg); } @Override public boolean onTouchEvent(MotionEvent event) { mTouchX = (int) event.getX(); mTouchY = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //ACTION_DOWN的作用就是判斷觸摸的點 Rect rect = new Rect(); int[] location = new int[2]; mIvMsgCountBg.getDrawingRect(rect); mIvMsgCountBg.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為ture則表示開始繪製開始 isTouch = true; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: //當抬起的時候,先判斷是否斷開了,才會出發動畫,並且訊息數量提示消失 //如果沒有斷開,則回到原點 if (isBroken) { boolean isPositionNoChanged = false; //如果彈起位置跟起始位置相差不大,則回到起始位置,不消失 if (Math.abs(mTouchX - mStartX) < TOUCH_SLOP && Math.abs(mTouchY - mStartY) < TOUCH_SLOP) { isPositionNoChanged = true; } if (isPositionNoChanged) { break; } else { mIvMsgCountBg.setVisibility(INVISIBLE); mIvClearAnim.setVisibility(View.VISIBLE); mIvClearAnim.setX(mTouchX - mIvClearAnim.getWidth() / 2); mIvClearAnim.setY(mTouchY - mIvClearAnim.getHeight() / 2); mIvClearAnim.setImageResource(R.drawable.tip_anim); ((AnimationDrawable) mIvClearAnim.getDrawable()).stop(); ((AnimationDrawable) mIvClearAnim.getDrawable()).start(); mPath.reset(); new Handler().postDelayed(new Runnable() { @Override public void run() { mIvMsgCountBg.setVisibility(VISIBLE); } }, 1200); } isBroken = false; } isTouch = false; mIvMsgCountBg.setX(mStartX - mIvMsgCountBg.getWidth() / 2); mIvMsgCountBg.setY(mStartY - mIvMsgCountBg.getHeight() / 2); break; } mCenterX = (mTouchX + mStartX) / 2; mCenterY = (mTouchY + mStartY) / 2; if (isTouch) { mIvMsgCountBg.setX(mTouchX - mIvMsgCountBg.getWidth() / 2); mIvMsgCountBg.setY(mTouchY - mIvMsgCountBg.getHeight() / 2); } invalidateView(); return true; } public void invalidateView() { if (Looper.myLooper() == Looper.getMainLooper()) { invalidate(); } else { postInvalidate(); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { mIvClearAnim.setX(mStartX); mIvClearAnim.setY(mStartY); mIvMsgCountBg.setX(mStartX - mIvMsgCountBg.getWidth() / 2); mIvMsgCountBg.setY(mStartY - mIvMsgCountBg.getHeight() / 2); super.onLayout(changed, l, t, r, b); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //如果在touch中,且線沒有斷掉,則繼續繪製,會澤清楚畫布. if (isTouch && !isBroken) { checkDragLen(); canvas.drawPath(mPath, mPaint); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY); canvas.drawCircle(mStartX, mStartY, mRaduis, mPaint); canvas.drawCircle(mTouchX, mTouchY, mRaduis, mPaint); } else { //相當於清楚繪製資訊 canvas.drawCircle(mStartX, mStartY, 0, mPaint); canvas.drawCircle(mTouchX, mTouchY, 0, mPaint); canvas.drawLine(0, 0, 0, 0, mPaint); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY); } } /** * 檢查拖拽長度是不是夠了,超過一定長度就斷開,不繪製了 */ private void checkDragLen() { int len = (int) Math.sqrt(Math.pow(mTouchX - mStartX, 2) + Math.pow(mTouchY - mStartY, 2)); mRaduis = -len / 15 + DEFAULT_RADIUS; //如果長度超過最大長度,isBroken設定為true,在後續就不繪製path了 if (len > DRAG_MAX_LEN) { isBroken = true; return; } //得到繪製貝茲路徑需要的四個點 float offsetX = (float) (mRaduis * Math.sin(Math.atan((mTouchY - mStartY) / (mTouchX - mStartX)))); float offsetY = (float) (mRaduis * Math.cos(Math.atan((mTouchY - mStartY) / (mTouchX - mStartX)))); float x1 = mStartX - offsetX; float y1 = mStartY + offsetY; float x2 = mTouchX - offsetX; float y2 = mTouchY + offsetY; float x3 = mTouchX + offsetX; float y3 = mTouchY - offsetY; float x4 = mStartX + offsetX; float y4 = mStartY - offsetY; mPath.reset(); mPath.moveTo(x1, y1); mPath.quadTo(mCenterX, mCenterY, x2, y2); mPath.lineTo(x3, y3); mPath.quadTo(mCenterX, mCenterY, x4, y4); mPath.lineTo(x1, y1); }}
使用activity_main.xml
Activity
package rander.com.bezier;import android.os.Bundle;import android.support.v7.app.AppCompatActivity;public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}
效果就是上面的實線效果