編輯:關於Android編程
這兩天學習了使用Path繪制貝塞爾曲線相關,然後自己動手做了一個類似QQ未讀消息可拖拽的小氣泡,效果圖如下:
接下來一步一步的實現整個過程。
其實就是使用Path繪制三點的二次方貝塞爾曲線來完成那個妖娆的曲線的。然後根據觸摸點不斷繪制對應的圓形,根據距離的改變改變原始固定圓形的半徑大小。最後就是松手後返回或者爆裂的實現。
顧名思義,就是一個路徑的意思,Path裡面有很多的方法,本次設計主要用到的相關方法有
moveTo()
移動Path到一個指定的點
quadTo()
繪制二次貝塞爾曲線,接收兩個點,第一個是控制弧度的點,第二個是終點。
lineTo()
就是連線
close()
閉合Path路徑,
reset()
重置Path的相關設置
Path入門熱身:
path.reset();
path.moveTo(200, 200);
//第一個坐標是對應的控制的坐標,第二個坐標是終點坐標
path.quadTo(400, 250, 600, 200);
canvas.drawPath(path, paint);
canvas.translate(0, 200);
//調用close,就會首尾閉合連接
path.close();
canvas.drawPath(path, paint);
記得不要在onDraw方法中new Path
或者 Paint
喲!
其實整個過程就是繪制了兩個貝塞爾二次曲線的的閉合Path路徑,然後在上面添加兩個圓形。
閉合的Path
路徑實現從左上點畫二次貝塞爾曲線到左下點,左下點連線到右下點,右下點二次貝塞爾曲線到右上點,最後閉合一下!!
相關坐標的確定
這是這次裡面的難點之一,因為涉及到了數學裡面的一個sin,cos,tan等等,我其實也忘完了,然後又腦補了一下,廢話不多說,直接上圖!!
vcex6szlz7XKx9PQwb3M17XEo6zI57n7vs3KudPD0rvM18C0u621xLuwo6y+zbuts/bP1tTa0P3XqrXEuf2zzNbQx/rP39bYtf7U2tK7xvC1xMfpv/ajoTwvc3Ryb25nPjwvcD4NCjxwPs7KzOLS0b6txdez9sC0wcujrL3Tz8LAtNaxvdO/tL+0tPrC68q1z9ajoTwvcD4NCjxoMiBpZD0="角度確定">角度確定
根據貼出來的原理圖可以知道,我們可以使用起始圓心坐標和拖拽的圓心坐標,根據反正切函數來得到具體的弧度。
int dy = Math.abs(CIRCLEY - startY);
int dx = Math.abs(CIRCLEX - startX);
angle = Math.atan(dy * 1.0 / dx);
ok,這裡的startX,Y就是移動過程中的坐標。angle就是得到的對應的弧度(角度)。
前面已經提到在旋轉的過程中有兩套坐標體系,一開始我也很糾結這個坐標體系要怎麼確定,後面又恍然大悟,其實相當於就是一三象限正比例增長,二四象限,反比例增長。
flag = (startY - CIRCLEY ) * (startX- CIRCLEX ) <= 0;
//增加一個flag,用於判斷使用哪種坐標體系。
最最重要的來了,繪制相關的Path路徑!
path.reset();
if (flag) {
//第一個點
path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO));
path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO));
path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO));
path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO));
path.close();
canvas.drawPath(path, paint);
} else {
//第一個點
path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO));
path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO));
path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO));
path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO));
path.close();
canvas.drawPath(path, paint);
}
這裡的代碼就是把圖片上相關的數學公式Java化而已!
到這裡,其實主要的工作就完成的差不多了!
接下來,設置paint
為填充的效果,最後再畫兩個圓
paint.setStyle(Paint.Style.FILL)
canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默認的
canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的
就可以繪制出想要的效果了!
這裡不得不再說說onTouch
的處理!
case MotionEvent.ACTION_DOWN://有事件先攔截再說!!
getParent().requestDisallowInterceptTouchEvent(true);
CurrentState = STATE_IDLE;
animSetXY.cancel();
startX = (int) ev.getX();
startY = (int) ev.getRawY();
break;
處理一下事件分發的坑!
這樣基本過得去了,但是我們的布局什麼的還沒有處理,math_parent是萬萬沒法使用到具體項目當中去的!
測量的時候,如果發現不是精准模式,那麼都手動去計算出需要的寬度和高度。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
if (modeWidth == MeasureSpec.UNSPECIFIED || modeWidth == MeasureSpec.AT_MOST) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_RADIO * 2, MeasureSpec.EXACTLY);
}
if (modeHeight == MeasureSpec.UNSPECIFIED || modeHeight == MeasureSpec.AT_MOST) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_RADIO * 2, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
然後在布局變化時,獲取相關坐標,確定初始圓心坐標:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
CIRCLEX = (int) ((w) * 0.5 + 0.5);
CIRCLEY = (int) ((h) * 0.5 + 0.5);
}
然後清單文件裡面就可以這樣配置了:
這樣之後,又會出現一個問題,那就是wrap_content
之後,這個View能繪制的區域只有自身那麼大了,拖拽了都看不見了!這個坑怎麼辦呢,其實很簡單,父布局加上android:clipChildren="false"
的屬性!
這個坑也算是解決了!!
我們是不希望它可以無限的拖拽的,就是有一個拖拽的最遠距離,還有就是放手後的返回,爆裂。那麼對應的,這裡需要確定幾種狀態:
private final static int STATE_IDLE = 1;//靜止的狀態
private final static int STATE_DRAG_NORMAL = 2;//正在拖拽的狀態
private final static int STATE_DRAG_BREAK = 3;//斷裂後的拖拽狀態
private final static int STATE_UP_BREAK = 4;//放手後的爆裂的狀態
private final static int STATE_UP_BACK = 5;//放手後的沒有斷裂的返回的狀態
private final static int STATE_UP_DRAG_BREAK_BACK = 6;//拖拽斷裂又返回的狀態
private int CurrentState = STATE_IDLE;
private int MIN_RADIO = (int) (ORIGIN_RADIO * 0.4);//最小半徑
private int MAXDISTANCE = (int) (MIN_RADIO * 13);//最遠的拖拽距離
確定好這些之後,在move的時候,就要去做相關判斷了:
case MotionEvent.ACTION_MOVE://移動的時候
startX = (int) ev.getX();
startY = (int) ev.getY();
updatePath();
invalidate();
break;
private void updatePath() {
int dy = Math.abs(CIRCLEY - startY);
int dx = Math.abs(CIRCLEX - startX);
double dis = Math.sqrt(dy * dy + dx * dx);
if (dis <= MAXDISTANCE) {//增加的情況,原始半徑減小
if (CurrentState == STATE_DRAG_BREAK || CurrentState == STATE_UP_DRAG_BREAK_BACK) {
CurrentState = STATE_UP_DRAG_BREAK_BACK;
} else {
CurrentState = STATE_DRAG_NORMAL;
}
ORIGIN_RADIO = (int) (DEFAULT_RADIO - (dis / MAXDISTANCE) * (DEFAULT_RADIO - MIN_RADIO));
Log.e(TAG, "distance: " + (int) ((1 - dis / MAXDISTANCE) * MIN_RADIO));
Log.i(TAG, "distance: " + ORIGIN_RADIO);
} else {
CurrentState = STATE_DRAG_BREAK;
}
// distance = dis;
flag = (startY - CIRCLEY) * (startX - CIRCLEX) <= 0;
Log.i("TAG", "updatePath: " + flag);
angle = Math.atan(dy * 1.0 / dx);
}
updatePath()
的方法之前已經看過部分了,這次的就是完整的。
這裡做的事就是根據拖拽的距離更改相關的狀態,並根據百分比來修改原始圓形的半徑大小。還有就是之前介紹的確定相關的弧度!
最後放手的時候:
case MotionEvent.ACTION_UP:
if (CurrentState == STATE_DRAG_NORMAL) {
CurrentState = STATE_UP_BACK;
valueX.setIntValues(startX, CIRCLEX);
valueY.setIntValues(startY, CIRCLEY);
animSetXY.start();
} else if (CurrentState == STATE_DRAG_BREAK) {
CurrentState = STATE_UP_BREAK;
invalidate();
} else {
CurrentState = STATE_UP_DRAG_BREAK_BACK;
valueX.setIntValues(startX, CIRCLEX);
valueY.setIntValues(startY, CIRCLEY);
animSetXY.start();
}
break;
自動返回這裡使用到的 ValueAnimator
,
animSetXY = new AnimatorSet();
valueX = ValueAnimator.ofInt(startX, CIRCLEX);
valueY = ValueAnimator.ofInt(startY, CIRCLEY);
animSetXY.playTogether(valueX, valueY);
valueX.setDuration(500);
valueY.setDuration(500);
valueX.setInterpolator(new OvershootInterpolator());
valueY.setInterpolator(new OvershootInterpolator());
valueX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
startX = (int) animation.getAnimatedValue();
Log.e(TAG, "onAnimationUpdate-startX: " + startX);
invalidate();
}
});
valueY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
startY = (int) animation.getAnimatedValue();
Log.e(TAG, "onAnimationUpdate-startY: " + startY);
invalidate();
}
});
最後在看看完整的onDraw
方法吧!
@Override
protected void onDraw(Canvas canvas) {
switch (CurrentState) {
case STATE_IDLE://空閒狀態,就畫默認的圓
if (showCircle) {
canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默認的
}
break;
case STATE_UP_BACK://執行返回的動畫
case STATE_DRAG_NORMAL://拖拽狀態 畫貝塞爾曲線和兩個圓
path.reset();
if (flag) {
//第一個點
path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO));
path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO));
path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO));
path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO));
path.close();
canvas.drawPath(path, paint);
} else {
//第一個點
path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO));
path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO));
path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO));
path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO));
path.close();
canvas.drawPath(path, paint);
}
if (showCircle) {
canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默認的
canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的
}
break;
case STATE_DRAG_BREAK://拖拽到了上限,畫拖拽的圓:
case STATE_UP_DRAG_BREAK_BACK:
if (showCircle) {
canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的
}
break;
case STATE_UP_BREAK://畫出爆裂的效果
canvas.drawCircle(startX - 25, startY - 25, 10, circlePaint);
canvas.drawCircle(startX + 25, startY + 25, 10, circlePaint);
canvas.drawCircle(startX, startY - 25, 10, circlePaint);
canvas.drawCircle(startX, startY, 18, circlePaint);
canvas.drawCircle(startX - 25, startY, 10, circlePaint);
break;
}
}
到這裡,成品就出來了!!
1、確定默認圓形的坐標;
2、根據move的情況,實時獲取最新的坐標,根據移動的距離(確定出角度),更新相關的狀態,畫出相關的Path路徑。超出上限,不再畫Path路徑。
3、松手時,根據相關的狀態,要麼帶Path路徑執行動畫返回,要麼不帶Path路徑直接返回,要麼直接爆裂!
Service翻譯成中文是服務,熟悉Windows 系統的同學一定很熟悉了。Android裡的Service跟Windows裡的Service功能差不多,就是一個不可見的
DatePicker實戰效果圖:依賴導入compile 'cn.aigestudio.datepicker:DatePicker:2.2.0'DatePi
下載其實我個人建議:使用綠色版,以後升級也方便,當然以後換電腦,就是超級方便,解壓完後再打開即用。綠色版也和chrome一樣,分Canary版,Dev版,Beta版,但是
今天我們開始進入講解android中的一些高級主題的用法,比如傳感器、GPS、NFC、語音和人臉識別等。這次來對傳感器的一個簡單介紹:Android平台支持三大類的傳感器