Android 實現IOS上的水滴效果控制元件
看到ios版上QQ重新整理效果像水滴,然後自己也想著去實現這樣的效果,這篇文章暫時沒有介紹下拉重新整理的效果,只是單獨用一個控制元件來實現這樣的水滴效果。
效果圖如下:
一、總體思路
1、畫兩個圓形,其中一個就是上面的大圓,還有一個是下面的小圓,大圓和小圓不斷變小,大圓的位置保持不變,小圓的位置不斷向下移動,即圓心不斷下移。
2、畫兩邊的曲線,這時候用到貝塞爾曲線去畫。
3、用屬性動畫實現動態的效果。
二、程式碼實現
1、找出畫曲線的幾個關鍵點。
其實我是在第一張圖的基礎上,再在上面分別畫兩個圓,就可以得到第二張圖了。關鍵是畫出第一張圖。
(1)在這裡,p1,p2,p3,p4,這4個點分別對應兩個圓的兩邊的點,即p1到p2就是圓的直徑。p3和p4同理,那麼就很容易確定這四個點的座標了。
(2)然後c1和c2是分別控制p1到p3、p2到p4的曲線,是貝塞爾曲線的控制點。它們的橫座標對應的是p3,p4的橫座標(相等),縱座標取兩個圓心距離的一半。這樣畫出這個靜態的圖片就不難了。
(3)畫上下兩個圓進去,就會變成第二張圖的效果。
2、在構造方法中呼叫init()初始化一些基本的變數
private void init(Context context, AttributeSet attrs) {
drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG
| Paint.FILTER_BITMAP_FLAG);
paint = new Paint();
paint.setColor(fillColor);
// 轉換為畫素單位
bigRadius = dip2px(context, bigRadius);
smallRadius = dip2px(context, smallRadius);
distance = dip2px(context, distance);
}
3、在onDraw()方法中畫水滴效果
要注意的是path需要重新new,
貝塞爾曲線的繪製,用到是path.quadTo這方法。具體可以看程式碼。
@Override
protected void onDraw(Canvas canvas) {
// 必須重新new,不然路徑會重複,我之前就是這樣
path = new Path();
// 把畫布移到中心
canvas.translate(width / 2, height / 2);
// 從canvas層面去除繪製時鋸齒
canvas.setDrawFilter(drawFilter);
// 當前的兩個圓心的距離
currentDis = distance * fraction;
// 計算當前大圓的半徑
float bigRadius = this.bigRadius - currentDis / bigPercent;
float smallRadius = 0;
if (currentDis > 5) {// 距離大於5才改變小圓的半徑
smallRadius = this.smallRadius - currentDis / smallPercent;
}
// 大圓兩邊的兩個點座標
leftX = -bigRadius;// 大圓當前的半徑
leftY = rightY = 0;
rightX = bigRadius;// 大圓當前的半徑
// 小圓兩邊的兩個點座標
leftX2 = -smallRadius;// 小圓當前的半徑
leftY2 = rightY2 = currentDis;
rightX2 = -leftX2; // 小圓當前的半徑
// 兩個控制點
controlX1 = -smallRadius;// x座標取小圓當前的半徑大小
controlY1 = currentDis / 2;// y座標取兩個圓距離的一半
controlX2 = smallRadius;// x座標取小圓當前的半徑大小
controlY2 = currentDis / 2;// y座標取兩個圓距離的一半
path.moveTo(leftX, leftY);
path.lineTo(rightX, rightY);
// 用二階貝塞爾曲線畫右邊的曲線,引數的第一個點是右邊的一個控制點
path.quadTo(controlX2, controlY2, rightX2, rightY2);
path.lineTo(leftX2, leftY2);
// 用二階貝塞爾曲線畫左邊邊的曲線,引數的第一個點是左邊的一個控制點
path.quadTo(controlX1, controlY1, leftX, leftY);
// 畫大圓
canvas.drawCircle(0, 0, bigRadius, paint);
// 畫小圓
canvas.drawCircle(0, currentDis, smallRadius, paint);
// 畫path
canvas.drawPath(path, paint);
}
4、用屬性動畫,實現動態的效果。
/*** 執行屬性動畫,實現水滴的效果 */
public void perforAnim() {
ValueAnimator valAnimator = ObjectAnimator.ofFloat(0, 1);
valAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
fraction = (float) animation.getAnimatedValue();
postInvalidate();
}
});
valAnimator.setDuration(duration);
valAnimator.start();
}
5、重寫onMeasure()方法,處理wrap_content情況。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/*
* 處理為wrap_content情況,那麼它的specMode是AT_MOST模式,在這種模式下它的寬/高
* 等於spectSize,這種情況下view的spectSize是parentSize,而parentSize是
* 父容器目前可以使用大小,就是父容器當前剩餘的空間大小, 就相當於使用match_parent一樣 的效果,因此我們可以設定一個預設的值
*/
int widthSpectMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpectSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpectMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpectSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpectMode == MeasureSpec.AT_MOST
&& heightSpectMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(width, height);
} else if (widthSpectMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(width, heightSpectSize);
} else if (heightSpectMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpectSize, height);
}
}
6、其它的一些方法實現。
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed) {
width = right - left;
height = bottom - top;
}
}
/**
* 根據手機的解析度從 dp 的單位 轉成為 px(畫素)
*/
public int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
7、欄位的定義
private final int fillColor = 0xff999999;// 填充顏色
private Paint paint;
private int width = 100, height = 300;// 預設寬高
/* 兩個圓心的最大距離 /
private int distance = 60;
/* 當前兩個圓心的距離 /
private float currentDis = 0;
private float bigRadius = 20;// 大圓半徑
private float smallRadius = 10;// 小圓半徑
private float controlX1, controlX2, controlY1, controlY2;// 兩個控制點的座標
private float leftX, leftY, rightX, rightY;// 大圓兩邊的兩個點的座標
private float leftX2, leftY2, rightX2, rightY2; // 小圓兩邊的兩個座標
DrawFilter drawFilter;
Path path;
/* 由屬性動畫控制,範圍為0-1 */
float fraction = 0;// 比例值
/* 大圓半徑變化的比例 /
private final int bigPercent = 8;
/* 小圓半徑變化的比例 /
private final int smallPercent = 20;
// 動畫的執行時間
private long duration = 3000;
三、總結
一種動畫效果,應該先分析它的靜態的實現,然後新增動態的效果,這樣就比較容易實現它的動畫效果了。
參考文章