編輯:關於android開發
純屬好奇心驅動寫的一個學習性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);
}
}
效果就是上面的實線效果
Android音樂播放器源碼(歌詞.均衡器.收藏.qq5.0菜單.通知),android.qq5.0一款Android音樂播放器源碼,基本功能都實現了 qq5.0菜單(歌
計算器Pro應用項目源碼,計算器pro源碼 本計算器實現了一些簡單的功能,可能本身還存在一些缺陷,希望大家提建議,能夠改進一下。 源碼項目我已經上傳到源碼天堂那
Linux內核系列—9.操作系統開發之Loader,linuxloader一個操作系統從開機到開始運行,大致經歷“引導—>加載內核入內存&m
android:自定義HorizontalScrollView實現qq側滑菜單 今天看了鴻洋_大神在慕課網講的qq5.0側滑菜單。學了不少的知識,同時也佩服鴻洋_大神