早在很久很久以前,QQ就實現(xiàn)了“一鍵下班”功能。何為“一鍵下班”?當(dāng)你QQ有信息時,下部會有信息數(shù)量提示紅點,點擊拖動之后,就會出現(xiàn)“一鍵下班”效果。本文將結(jié)合github上關(guān)于此功能的一個簡單實現(xiàn),介紹這個功能的基本實現(xiàn)思路。
項目地址
https://github.com/chenupt/BezierDemo
最終實現(xiàn)效果
實現(xiàn)原理解析
我個人感覺,這個效果實現(xiàn)的很漂亮啊!那么咱們就來看看實現(xiàn)原理是什么~
注:下面內(nèi)容請參照項目源碼觀看。
其實如果從代碼來看,實現(xiàn)的過程并不復(fù)雜,重點需要掌握的就是
- path的用法
- 貝塞爾曲線的使用。
這個項目的核心就是BezierView,繼承自FrameLayout,拖動的時候,相當(dāng)于覆蓋在屏幕上一樣。在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對象,然后動態(tài)生成了兩個ImageView
- exploredImageView 主要用來實現(xiàn)爆炸效果,默認不可見
- tipImageView 手指進行拖動時的紅色圖標
exploredImageView設(shè)置的圖片資源是一個AnimationDrawable,下面是res中的聲明,控制每張圖片的播放順序和持續(xù)時間,這也很好理解
<?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>
我們在學(xué)習(xí)這種自定義控件的時候,可以按照View的繪制過程,對代碼進行重點的查看,比如說,我們可以從
下面這個順序來對這個項目進行學(xué)習(xí)。
- 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的位置,在這里出現(xiàn)了兩個變量,startX和startY,這兩個變量控制的是紅點的初始化坐標,在整個過程中不會發(fā)生改變。
那么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()里面的操作也并不復(fù)雜,如果正在執(zhí)行動畫或者是沒有在觸摸模式,就畫一個透明的顏色,否則,就開始畫真正的界面了。calculate()這個方法是一個重點,從命名來看應(yīng)該是計算了一些坐標值,然后開始畫了兩個圓,這兩個圓的坐標,一個是(startX,startY),另一個是(x,y),顏色和半徑都是相同的,這個是為了簡化計算,所以將兩個圓的半徑設(shè)置成相同的啦。
我們先繼續(xù)看一下在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的屏幕坐標位置,然后根據(jù)觸摸點的位置,來判斷是否是觸摸狀態(tài),從而改變isTouch的取值,而如果不是按下時間,則推出改變isTouch,從而觸摸狀態(tài),還原tipImageVIew的位置。但是無論如何,都會執(zhí)行invalidate(),來調(diào)用onDraw(),在那里面就執(zhí)行了實際的畫圓的操作,這個咱們一會在看。再往下呢,就是根據(jù)動畫狀態(tài)是否正在播放,來更新x、y坐標,還有anchorX和anchorY的值。
x和y的值其實就是觸摸點的位置,主要用來控制手指所按下的圓的位置,那么anchorX和anchorY呢?這兩個值其實就是控制錨點的坐標,用于貝塞爾曲線的繪制。
說到了這里,我相信你應(yīng)該明白實現(xiàn)的基本思路了,但是最重要的,就是拉扯效果,到底是如何實現(xiàn)的呢?那么咱們就來看一下最重要的calculate()到底做了些什么!
在看這段代碼之前,咱們先簡單學(xué)習(xí)一下貝塞爾曲線及如何繪制。
貝塞爾曲線于1962年由法國數(shù)學(xué)家Pierre Bézier第一次研究使用并給出了詳細的計算公式,So該曲線也是由其名字命名。
Path中給出的quadTo方法屬于二階貝賽爾曲線。
來看下高清無碼GIF動圖,從愛哥那邊偷的,別告訴他_
從上面的動圖中,我們可以發(fā)現(xiàn),二階貝塞爾曲線,我們只需要確定三個點,就可以畫出一條平滑的曲線,P0和P2是起點和終點,而P1就是我們的錨點,也就是前面提到的anchorX和anchorY。
那么問題來了,如果我們要實現(xiàn)這種拖拽拉伸的效果,需要知道幾個點呢?
先來張設(shè)計圖
可以看到,在設(shè)計圖中,有P1-P4四個坐標點,是兩條圓外切線與圓的交點坐標,因為需要P1-P2和P3-P4兩條貝塞爾曲線的歪曲程度相同,所以錨點只需要在P0到原點坐標的連線上取一個點即可,所以,咱們就需要5個坐標點。
容我喝口水_
來來來,咱們繼續(xù)!
那么,既然知道了需要哪五個坐標點,anchorX和anchorY在onTouchEvent()里面已經(jīng)算出來了,那么,剩下的4個坐標點怎么求呢?其實這就是calculate()內(nèi)部所做的主要工作。
由于將兩個圓的半徑設(shè)置為相同,可以精簡計算,所以下面的代碼也是假設(shè)兩個圓的半徑相同進行操作的,凱子哥再給你手繪一張高清無碼大圖
startX和startY是指定值,這里我們以它為坐標原點,另外一個圓的坐標為(x,y),即手指觸摸的位置坐標,兩圓半徑相同,則外切線平行,過(x,y)點做垂直線垂直于兩條切線。
現(xiàn)在,已知(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))));
那么。,,現(xiàn)在再來看下面的代碼,你還說你看不懂嗎?
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);
}
// 根據(jù)角度算出四邊形的四個點
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啦
肚子餓了,這一篇就到這里了,下去吃飯飯
相關(guān)項目及文章
相關(guān)項目
尊重原創(chuàng),轉(zhuǎn)載請注明:From 凱子哥(http://blog.csdn.net/zhaokaiqiang1992) 侵權(quán)必究!