欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時(shí)間:2017年04月28日 14:39:40   投稿:daisy  
最近發(fā)現(xiàn)QQ討論組的頭像非常不錯(cuò),正好最近又有時(shí)間,所有就動(dòng)手實(shí)踐了下,所以下面這篇文章主要給大家介紹了Android自定義View模仿QQ討論組頭像效果的相關(guān)資料,文中給出了詳細(xì)的介紹的示例代碼,需要的朋友可以參考學(xué)習(xí),下面來一起看看吧。

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

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

下面就讓我們開始實(shí)現(xiàn)吧。

布局

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

布局

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

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

公式1

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

公式2

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

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

// 頭像信息類,記錄大小、位置等信息
private static class DrawableInfo {
 int mId = View.NO_ID;
 Drawable mDrawable;
 // 中心點(diǎn)位置
 float mCenterX;
 float mCenterY;
 // 頭像上缺口弧所在圓上的圓心位置,其實(shí)就是下一個(gè)相鄰頭像的中心點(diǎn)
 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);
  // 以所有圖像圓為內(nèi)切圓的圓的半徑
  final float R = (float) (r * ((sinN + 1) / sinN));
  mOffsetY = (float) ((mContentSize - R - r * (1 + 1 / Math.tan(Math.PI / N))) / 2f);
 }

 // 初始化第一個(gè)頭像的中心位置
 final float startX, startY;
 if (N % 2 == 0) {
  startX = startY = r;
 } else {
  startX = center;
  startY = r;
 }

 // 變換矩陣
 final Matrix matrix = mLayoutMatrix;
 // 坐標(biāo)點(diǎn)臨時(shí)數(shù)組
 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) {
  // 以上一個(gè)圓的圓心旋轉(zhuǎn)計(jì)算得出當(dāng)前圓的圓位置
  matrix.postRotate(360.f / N, center, center + mOffsetY);
  matrix.mapPoints(pointsTemp);
  }

  // 取出中心點(diǎn)位置
  drawable.mCenterX = pointsTemp[0];
  drawable.mCenterY = pointsTemp[1];

  // 設(shè)置邊界
  drawable.mBounds.inset(-r, -r);
  drawable.mBounds.offset(drawable.mCenterX, drawable.mCenterY);

  // 設(shè)置“蒙板”路徑
  drawable.mMaskPath.addCircle(drawable.mCenterX, drawable.mCenterY, r, Path.Direction.CW);
  drawable.mMaskPath.setFillType(Path.FillType.INVERSE_WINDING);
 }

 // 設(shè)置第一個(gè)頭像的缺口,頭像數(shù)量少于3個(gè)的時(shí)候沒有
 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();
}

繪制

計(jì)算好每個(gè)頭像的大小和位置后,就可以把它們繪制出來了。但在此之前,還得先解決一個(gè)問題——如何使頭像圖像變圓?因?yàn)檩斎隓rawable對(duì)象并沒有任何限制。

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

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

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

下面來看一下 onDraw 方法:

@Override
protected 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);

  // 設(shè)置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 對(duì)象,那就不能像 Bitmap 那樣繪制出來就完事了的,除非你不打算支持Drawable的一些功能,如自更新、動(dòng)畫、狀態(tài)等。

1、Drawable自更新和動(dòng)畫Drawable

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

public interface Callback {
 /**
  * 當(dāng)drawable需要重新繪制時(shí)調(diào)用。此時(shí)的view應(yīng)該使其自身失效(至少drawable展示部分失效)
  * @param who 要求重新繪制的drawable
  */
 void invalidateDrawable(@NonNull Drawable who);

 /**
  * drawable可以通過調(diào)用該方法來安排動(dòng)畫的下一幀。
  * @param who 要預(yù)定的drawable
  * @param what 要執(zhí)行的動(dòng)作
  * @param when 執(zhí)行的時(shí)間(以毫秒為單位),基于android.os.SystemClock.uptimeMillis()
  */
 void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when);

 /**
  * drawable可以通過調(diào)用該方法來取消先前通過scheduleDrawable(Drawable, Runnable, long)調(diào)度的動(dòng)作。
  * @param who 要取消預(yù)定的drawable
  * @param what 要取消執(zhí)行的動(dòng)作
  */
 void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);
}

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

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

所以只是設(shè)置了Callback的話,當(dāng)Drawable內(nèi)容改變需要重新繪制時(shí)View還是不會(huì)更新重繪的,動(dòng)畫需要計(jì)劃下一幀或者取消一個(gè)計(jì)劃時(shí)也不會(huì)成功。因此我們也得驗(yàn)證自己的Drawable:

private boolean hasSameDrawable(Drawable drawable) {
 for (DrawableInfo d : mDrawables) {
  if (d.mDrawable == drawable) {
   return true;
  }
 }
 return false;
}

@Override
protected boolean verifyDrawable(@NonNull Drawable drawable) {
 return hasSameDrawable(drawable) || super.verifyDrawable(drawable);
}

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

下面展示一下效果:


AnimationDrawable

2、狀態(tài)

一些Drawable是有狀態(tài)的,它能根據(jù)View的狀態(tài)(按下,選中,激活等)改變其顯示內(nèi)容,如 StateListDrawable 。要支持View狀態(tài)的話,其實(shí)只要擴(kuò)展 View.drawableStateChanged() View.jumpDrawablesToCurrentState() 方法,當(dāng)View的狀態(tài)改變的時(shí)候更新Drawable的狀態(tài)就行了:

// 狀態(tài)改變時(shí)被調(diào)用
@Override
protected void drawableStateChanged() {
 super.drawableStateChanged();
 boolean invalidate = false;
 for (DrawableInfo drawable : mDrawables) {
  Drawable d = drawable.mDrawable;
  // 判斷Drawable是否支持狀態(tài)并更新狀態(tài)
  if (d.isStateful() && d.setState(getDrawableState())) {
   invalidate = true;
  }
 }
 if (invalidate) {
  invalidate();
 }
}

// 這個(gè)方法主要針對(duì)狀態(tài)改變時(shí)有過渡動(dòng)畫的Drawable
@Override
public void jumpDrawablesToCurrentState() {
 super.jumpDrawablesToCurrentState();
 for (DrawableInfo drawable : mDrawables) {
  drawable.mDrawable.jumpToCurrentState();
 }
}

效果:


狀態(tài)

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

其他效果展示:

效果1

效果2

項(xiàng)目主頁(yè):https://github.com/YiiGuxing/CompositionAvatar

本地下載:http://xiazai.jb51.net/201704/yuanma/CompositionAvatar-master(jb51.net).rar

總結(jié)

以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。

相關(guān)文章

最新評(píng)論