国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 系統 > Android > 正文

Android自定義View模仿QQ討論組頭像效果

2019-10-23 18:29:58
字體:
來源:轉載
供稿:網友

首先來看看我們模仿的效果圖,相信對于使用過QQ的人來說都不陌生,效果圖如下:

android,仿qq頭像,qq討論組頭像,qq頭像

在以前的一個項目中,需要實現類似QQ討論組頭像的控件,只是頭像數量和布局有一小點不一樣:一是最頭像數是4個,二是頭像數是2個時的布局是橫著排的。其實當時GitHub上就有類似的開源控件,只是那個控件在每一次繪制View的時候都會新創建一些Bitmap對象,這肯定是不可取的,而且那個控件頭像輸入的是Bitmap對象,不滿足需求。所以只能自己實現一個了。實現的時候也沒有過多的考慮,傳入頭像Drawable對象,根據數量排列顯示就算完成了,而且傳入的圖像還必需是圓形的,限制很大,根本不具備通用性。因此要實現和QQ討論組頭像一樣的又具備一定通用性的控件,還得重新設計、實現。

下面就讓我們開始實現吧。

布局

首先需要解決的是頭像的布局,在頭像數量分別為1至5的情況下,定義頭像的布局排列方式,并計算出圖像的大小和位置。先把布局圖畫出來再說:

android,仿qq頭像,qq討論組頭像,qq頭像

布局

其中黑色正方形就是View的顯示區,藍色圓形就是頭像了。已知的條件是View大小,姑且設為 D 吧,還有頭像的數量 n ,求藍色圓的半徑 r 及圓心位置。這不就是一道幾何題嗎?翻開初中的數學課本——勾三股四弦五……好像不夠用啊……

輔助線畫了又畫,頭皮撓了又撓,α,θ,OMG......sin,cos,sh*t......終于算出了 r 與 D 和 n 的關系:

android,仿qq頭像,qq討論組頭像,qq頭像

公式1

其實 n=3 的時候半徑和 n=4 的時候是一樣的,但是考慮到 n=3,5 時在Y軸上還有一個偏移量 dy ,而且 r 和 dy 在 n=3,5 時是有通式的,所以就合在一起了。求偏移量 dy 的公式:

android,仿qq頭像,qq討論組頭像,qq頭像

公式2

式中 R 就是布局圖中紅色大圓的半徑。

有了公式,那么代碼就好寫了,計算每個頭像的大小和位置的代碼如下:

// 頭像信息類,記錄大小、位置等信息private static class DrawableInfo { int mId = View.NO_ID; Drawable mDrawable; // 中心點位置 float mCenterX; float mCenterY; // 頭像上缺口弧所在圓上的圓心位置,其實就是下一個相鄰頭像的中心點 float mGapCenterX; float mGapCenterY; boolean mHasGap; // 頭像邊界 final RectF mBounds = new RectF(); // 圓形蒙板路徑,把頭像弄成圓形 final Path mMaskPath = new Path();}
private void layoutDrawables() { mSteinerCircleRadius = 0; mOffsetY = 0; int width = getWidth() - getPaddingLeft() - getPaddingRight(); int height = getHeight() - getPaddingTop() - getPaddingBottom(); mContentSize = Math.min(width, height); final List<DrawableInfo> drawables = mDrawables; final int N = drawables.size(); float center = mContentSize * .5f; if (mContentSize > 0 && N > 0) { // 圖像圓的半徑。 final float r; if (N == 1) {  r = mContentSize * .5f; } else if (N == 2) {  r = (float) (mContentSize / (2 + 2 * Math.sin(Math.PI / 4))); } else if (N == 4) {  r = mContentSize / 4.f; } else {  r = (float) (mContentSize / (2 * (2 * Math.sin(((N - 2) * Math.PI) / (2 * N)) + 1)));  final double sinN = Math.sin(Math.PI / N);  // 以所有圖像圓為內切圓的圓的半徑  final float R = (float) (r * ((sinN + 1) / sinN));  mOffsetY = (float) ((mContentSize - R - r * (1 + 1 / Math.tan(Math.PI / N))) / 2f); } // 初始化第一個頭像的中心位置 final float startX, startY; if (N % 2 == 0) {  startX = startY = r; } else {  startX = center;  startY = r; } // 變換矩陣 final Matrix matrix = mLayoutMatrix; // 坐標點臨時數組 final float[] pointsTemp = this.mPointsTemp; matrix.reset(); for (int i = 0; i < drawables.size(); i++) {  DrawableInfo drawable = drawables.get(i);  drawable.reset();  drawable.mHasGap = i > 0;  // 缺口弧的中心  if (drawable.mHasGap) {  drawable.mGapCenterX = pointsTemp[0];  drawable.mGapCenterY = pointsTemp[1];  }  pointsTemp[0] = startX;  pointsTemp[1] = startY;  if (i > 0) {  // 以上一個圓的圓心旋轉計算得出當前圓的圓位置  matrix.postRotate(360.f / N, center, center + mOffsetY);  matrix.mapPoints(pointsTemp);  }  // 取出中心點位置  drawable.mCenterX = pointsTemp[0];  drawable.mCenterY = pointsTemp[1];  // 設置邊界  drawable.mBounds.inset(-r, -r);  drawable.mBounds.offset(drawable.mCenterX, drawable.mCenterY);  // 設置“蒙板”路徑  drawable.mMaskPath.addCircle(drawable.mCenterX, drawable.mCenterY, r, Path.Direction.CW);  drawable.mMaskPath.setFillType(Path.FillType.INVERSE_WINDING); } // 設置第一個頭像的缺口,頭像數量少于3個的時候沒有 if (N > 2) {  DrawableInfo first = drawables.get(0);  DrawableInfo last = drawables.get(N - 1);  first.mHasGap = true;  first.mGapCenterX = last.mCenterX;  first.mGapCenterY = last.mCenterY; } mSteinerCircleRadius = r; } invalidate();}

繪制

計算好每個頭像的大小和位置后,就可以把它們繪制出來了。但在此之前,還得先解決一個問題——如何使頭像圖像變圓?因為輸入Drawable對象并沒有任何限制。

在上面的 layoutDrawables 方法中有這樣兩行代碼:

drawable.mMaskPath.addCircle(drawable.mCenterX, drawable.mCenterY, r, Path.Direction.CW);drawable.mMaskPath.setFillType(Path.FillType.INVERSE_WINDING);

其中第一行是添加一個圓形路徑,這個路徑就是布局圖中藍色圓的路徑,而第二行是設置路徑的填充模式,默認的填充模式是填充路徑內部,而 INVERSE_WINDING 模式是填充路徑外部,再配合 Paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)) 就可以繪制出圓形的圖像了。頭像上的缺口同理。(ps:關于Path.FillTypePorterDuff.Mode網上介紹挺多的,這里就不詳細介紹了)

下面來看一下 onDraw 方法:

@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); ... canvas.translate(0, mOffsetY); final Paint paint = mPaint; final float gapRadius = mSteinerCircleRadius * (mGap + 1f); for (int i = 0; i < drawables.size(); i++) {  DrawableInfo drawable = drawables.get(i);  RectF bounds = drawable.mBounds;  final int savedLayer = canvas.saveLayer(0, 0, mContentSize, mContentSize, null, Canvas.ALL_SAVE_FLAG);  // 設置Drawable的邊界  drawable.mDrawable.setBounds((int) bounds.left, (int) bounds.top,    Math.round(bounds.right), Math.round(bounds.bottom));  // 繪制Drawable  drawable.mDrawable.draw(canvas);  // 繪制“蒙板”路徑,將Drawable繪制的圖像“剪”成圓形  canvas.drawPath(drawable.mMaskPath, paint);  // “剪”出弧形的缺口  if (drawable.mHasGap && mGap > 0f) {   canvas.drawCircle(drawable.mGapCenterX, drawable.mGapCenterY, gapRadius, paint);  }  canvas.restoreToCount(savedLayer); }}

Drawable支持

既然輸入的是 Drawable 對象,那就不能像 Bitmap 那樣繪制出來就完事了的,除非你不打算支持Drawable的一些功能,如自更新、動畫、狀態等。

1、Drawable自更新和動畫Drawable

Drawable的自更新和動畫Drawable(如 AnimationDrawable , AnimatedVectorDrawable 等)都是依賴于 Drawable.Callback 接口。其定義如下:

public interface Callback { /**  * 當drawable需要重新繪制時調用。此時的view應該使其自身失效(至少drawable展示部分失效)  * @param who 要求重新繪制的drawable  */ void invalidateDrawable(@NonNull Drawable who); /**  * drawable可以通過調用該方法來安排動畫的下一幀。  * @param who 要預定的drawable  * @param what 要執行的動作  * @param when 執行的時間(以毫秒為單位),基于android/59012.html">android.os.SystemClock.uptimeMillis()  */ void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when); /**  * drawable可以通過調用該方法來取消先前通過scheduleDrawable(Drawable, Runnable, long)調度的動作。  * @param who 要取消預定的drawable  * @param what 要取消執行的動作  */ void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);}

所以要支持Drawable自更新和動畫Drawable,得通過 Drawable.setCallback(Drawable.Callback) 方法設置 Drawable.Callback 接口的實現對象才行。好在 android.view.View 已經實現了這個接口,在設置Drawable的時候調用一下 Drawable.setCallback(MyView.this) 即可。但需要注意的是, android.view.View 實現 Drawable.Callback 接口的時候都調用了 View.verifyDrawable(Drawable) 以驗證需要顯示更新的Drawable是不是自己的Drawable,且其實現只是驗證了View自己的背景和前景:

protected boolean verifyDrawable(@NonNull Drawable who) { // ... return who == mBackground || (mForegroundInfo != null && mForegroundInfo.mDrawable == who);}

所以只是設置了Callback的話,當Drawable內容改變需要重新繪制時View還是不會更新重繪的,動畫需要計劃下一幀或者取消一個計劃時也不會成功。因此我們也得驗證自己的Drawable:

private boolean hasSameDrawable(Drawable drawable) { for (DrawableInfo d : mDrawables) {  if (d.mDrawable == drawable) {   return true;  } } return false;}@Overrideprotected boolean verifyDrawable(@NonNull Drawable drawable) { return hasSameDrawable(drawable) || super.verifyDrawable(drawable);}

此時,Drawable自更新的支持和動畫Drawable的支持基本上是完成了。當然,View不可見和 onDetachedFromWindow() 時應該是要暫停或者停止動畫的,這些在這里就不多說了,可以去看源碼(在文章結尾處有鏈接),主要是調用 Drawable.setVisible(boolean, boolean) 方法。

下面展示一下效果:

android,仿qq頭像,qq討論組頭像,qq頭像
AnimationDrawable

2、狀態

一些Drawable是有狀態的,它能根據View的狀態(按下,選中,激活等)改變其顯示內容,如 StateListDrawable 。要支持View狀態的話,其實只要擴展 View.drawableStateChanged() View.jumpDrawablesToCurrentState() 方法,當View的狀態改變的時候更新Drawable的狀態就行了:

// 狀態改變時被調用@Overrideprotected void drawableStateChanged() { super.drawableStateChanged(); boolean invalidate = false; for (DrawableInfo drawable : mDrawables) {  Drawable d = drawable.mDrawable;  // 判斷Drawable是否支持狀態并更新狀態  if (d.isStateful() && d.setState(getDrawableState())) {   invalidate = true;  } } if (invalidate) {  invalidate(); }}// 這個方法主要針對狀態改變時有過渡動畫的Drawable@Overridepublic void jumpDrawablesToCurrentState() { super.jumpDrawablesToCurrentState(); for (DrawableInfo drawable : mDrawables) {  drawable.mDrawable.jumpToCurrentState(); }}

效果:

android,仿qq頭像,qq討論組頭像,qq頭像
狀態

好了,到這里控件算是完成了。

其他效果展示:

android,仿qq頭像,qq討論組頭像,qq頭像

效果1

android,仿qq頭像,qq討論組頭像,qq頭像

效果2

項目主頁:https://github.com/YiiGuxing/CompositionAvatar

 

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對VEVB武林網的支持。


注:相關教程知識閱讀請移步到Android開發頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 遂昌县| 中西区| 云龙县| 长寿区| 榕江县| 黄龙县| 乌拉特后旗| 吉安市| 固始县| 米脂县| 元朗区| 腾冲县| 微博| 多伦县| 汕头市| 安龙县| 潮州市| 平凉市| 铜山县| 汽车| 西充县| 军事| 华容县| 通河县| 和硕县| 高唐县| 苗栗县| 滕州市| 新昌县| 长治市| 宁远县| 九寨沟县| 寿阳县| 含山县| 通榆县| 六安市| 昆明市| 禄丰县| 长岭县| 舒城县| 岑溪市|