早在很久很久以前,QQ就實現了“一鍵下班”功能。何爲“一鍵下班”?當你QQ有信息時,下部會有信息數量提示紅點,點擊拖動之後,就會出現“一鍵下班”效果。本文將結合github上關於此功能的一個簡單實現,介紹這個功能的基本實現思路。
項目地址
https://github.com/chenupt/BezierDemo
最終實現效果
實現原理解析
我個人感覺,這個效果實現的很漂亮啊!那麼咱們就來看看實現原理是什麼~
注:下面內容請參照項目源碼觀看。
其實如果從代碼來看,實現的過程並不複雜,重點需要掌握的就是
- path的用法
- 貝塞爾曲線的使用。
這個項目的核心就是BezierView,繼承自FrameLayout,拖動的時候,相當於覆蓋在屏幕上一樣。在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);
}
初始化了Path和Paint對象,然後動態生成了兩個ImageView
- exploredImageView 主要用來實現爆炸效果,默認不可見
- tipImageView 手指進行拖動時的紅色圖標
exploredImageView設置的圖片資源是一個AnimationDrawable,下面是res中的聲明,控制每張圖片的播放順序和持續時間,這也很好理解
<?xml version="1.0" encoding="utf-8"?>
<animation-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true">
<item android:drawable="@drawable/idp" android:duration="300"/>
<item android:drawable="@drawable/idq" android:duration="300"/>
<item android:drawable="@drawable/idr" android:duration="300"/>
<item android:drawable="@drawable/ids" android:duration="300"/>
<item android:drawable="@drawable/idt" android:duration="300"/>
<item android:drawable="@android:color/transparent" android:duration="300"/>
</animation-list>
我們在學習這種自定義控件的時候,可以按照View的繪製過程,對代碼進行重點的查看,比如說,我們可以從
下面這個順序來對這個項目進行學習。
- onMeasure()
- onLayout()
- onDraw()
- onTouchEvent()
因爲這個項目沒有重寫onMeasure(),所以我們直接從onLayout看看做了什麼
@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);
}
代碼還是非常還理解的,無非就是初始化了ImageView的位置,在這裏出現了兩個變量,startX和startY,這兩個變量控制的是紅點的初始化座標,在整個過程中不會發生改變。
那麼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);
}
onDraw()裏面的操作也並不複雜,如果正在執行動畫或者是沒有在觸摸模式,就畫一個透明的顏色,否則,就開始畫真正的界面了。calculate()這個方法是一個重點,從命名來看應該是計算了一些座標值,然後開始畫了兩個圓,這兩個圓的座標,一個是(startX,startY),另一個是(x,y),顏色和半徑都是相同的,這個是爲了簡化計算,所以將兩個圓的半徑設置成相同的啦。
我們先繼續看一下在onTouchEvent()裏面進行了什麼操作
@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;
}
首先在按下的時候,取得了tipImageView的屏幕座標位置,然後根據觸摸點的位置,來判斷是否是觸摸狀態,從而改變isTouch的取值,而如果不是按下時間,則推出改變isTouch,從而觸摸狀態,還原tipImageVIew的位置。但是無論如何,都會執行invalidate(),來調用onDraw(),在那裏面就執行了實際的畫圓的操作,這個咱們一會在看。再往下呢,就是根據動畫狀態是否正在播放,來更新x、y座標,還有anchorX和anchorY的值。
x和y的值其實就是觸摸點的位置,主要用來控制手指所按下的圓的位置,那麼anchorX和anchorY呢?這兩個值其實就是控制錨點的座標,用於貝塞爾曲線的繪製。
說到了這裏,我相信你應該明白實現的基本思路了,但是最重要的,就是拉扯效果,到底是如何實現的呢?那麼咱們就來看一下最重要的calculate()到底做了些什麼!
在看這段代碼之前,咱們先簡單學習一下貝塞爾曲線及如何繪製。
貝塞爾曲線於1962年由法國數學家Pierre Bézier第一次研究使用並給出了詳細的計算公式,So該曲線也是由其名字命名。
Path中給出的quadTo方法屬於二階貝賽爾曲線。
來看下高清無碼GIF動圖,從愛哥那邊偷的,別告訴他^_^
從上面的動圖中,我們可以發現,二階貝塞爾曲線,我們只需要確定三個點,就可以畫出一條平滑的曲線,P0和P2是起點和終點,而P1就是我們的錨點,也就是前面提到的anchorX和anchorY。
那麼問題來了,如果我們要實現這種拖拽拉伸的效果,需要知道幾個點呢?
先來張設計圖
可以看到,在設計圖中,有P1-P4四個座標點,是兩條圓外切線與圓的交點座標,因爲需要P1-P2和P3-P4兩條貝塞爾曲線的歪曲程度相同,所以錨點只需要在P0到原點座標的連線上取一個點即可,所以,咱們就需要5個座標點。
容我喝口水^_^
來來來,咱們繼續!
那麼,既然知道了需要哪五個座標點,anchorX和anchorY在onTouchEvent()裏面已經算出來了,那麼,剩下的4個座標點怎麼求呢?其實這就是calculate()內部所做的主要工作。
由於將兩個圓的半徑設置爲相同,可以精簡計算,所以下面的代碼也是假設兩個圓的半徑相同進行操作的,凱子哥再給你手繪一張高清無碼大圖
startX和startY是指定值,這裏我們以它爲座標原點,另外一個圓的座標爲(x,y),即手指觸摸的位置座標,兩圓半徑相同,則外切線平行,過(x,y)點做垂直線垂直於兩條切線。
現在,已知(startX,startY),(x,y),半徑radius,還有個直角,因此,我們只需要知道一個角度,然後就可以求出offsetX和offsetY,也就求出P1-P4的四點座標了~~~
那麼這個角度好求麼?
簡單,再來張高清無碼大圖~
因爲
- ∠α=∠3
- ∠3+∠2=90
- ∠1+∠2=90
所以
∠α=∠1
這是初中的三段式麼…忘記了
那麼∠1怎麼求呢?簡單啊,(x,y)都知道了,
tan∠1= (y-startY)/(x-startX);
因此可得
∠1 = arctan((y-startY)/(x-startX))
知道角度,知道radius,還求不出offsetX和offsetY麼~
所以
float offsetX = (float) (radius*Math.sin(Math.atan((y - startY) / (x - startX))));
float offsetY = (float) (radius*Math.cos(Math.atan((y - startY) / (x - startX))));
那麼。,,現在再來看下面的代碼,你還說你看不懂嗎?
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);
}
算出4個點的座標,並且知道錨點位置,用path連起來就Ok啦
肚子餓了,這一篇就到這裏了,下去吃飯飯
相關項目及文章
相關項目
-
尊重原創,轉載請註明:From 凱子哥(http://blog.csdn.net/zhaokaiqiang1992) 侵權必究!
關注我的微博,可以獲得更多精彩內容