轉(zhuǎn)載文章作者:一息尚存? ?
原文鏈接:http://www.reibang.com/p/349aa6153fcc
效果圖:
在以前的一個項目中姨丈,需要實現(xiàn)類似QQ討論組頭像的控件听系,只是頭像數(shù)量和布局有一小點不一樣:一是最頭像數(shù)是4個,二是頭像數(shù)是2個時的布局是橫著排的藐守。其實當(dāng)時GitHub上就有類似的開源控件,只是那個控件在每一次繪制View的時候都會新創(chuàng)建一些Bitmap對象牙肝,這肯定是不可取的焰情,而且那個控件頭像輸入的是Bitmap對象搞疗,不滿足需求嗓蘑。所以只能自己實現(xiàn)一個了。實現(xiàn)的時候也沒有過多的考慮匿乃,傳入頭像Drawable對象桩皿,根據(jù)數(shù)量排列顯示就算完成了,而且傳入的圖像還必需是圓形的幢炸,限制很大泄隔,根本不具備通用性。因此要實現(xiàn)和QQ討論組頭像一樣的又具備一定通用性的控件宛徊,還得重新設(shè)計佛嬉、實現(xiàn)。下面就讓我們開始實現(xiàn)吧岩调。
布局
首先需要解決的是頭像的布局巷燥,在頭像數(shù)量分別為1至5的情況下赡盘,定義頭像的布局排列方式号枕,并計算出圖像的大小和位置。先把布局圖畫出來再說:
布局
其中黑色正方形就是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
其實 n=3 的時候半徑和 n=4 的時候是一樣的,但是考慮到 n=3,5 時在Y軸上還有一個偏移量 dy ,而且 r 和 dy 在 n=3,5 時是有通式的诲侮,所以就合在一起了镀虐。求偏移量 dy 的公式:
公式2
式中 R 就是布局圖中紅色大圓的半徑。
有了公式沟绪,那么代碼就好寫了刮便,計算每個頭像的大小和位置的代碼如下:
// 頭像信息類,記錄大小绽慈、位置等信息privatestaticclassDrawableInfo{intmId = View.NO_ID;? ? Drawable mDrawable;// 中心點位置floatmCenterX;floatmCenterY;// 頭像上缺口弧所在圓上的圓心位置恨旱,其實就是下一個相鄰頭像的中心點floatmGapCenterX;floatmGapCenterY;booleanmHasGap;// 頭像邊界finalRectF mBounds =newRectF();// 圓形蒙板路徑,把頭像弄成圓形finalPath mMaskPath =newPath();}
privatevoidlayoutDrawables(){? ? mSteinerCircleRadius =0;? ? mOffsetY =0;intwidth = getWidth() - getPaddingLeft() - getPaddingRight();intheight = getHeight() - getPaddingTop() - getPaddingBottom();? ? mContentSize = Math.min(width, height);finalList drawables = mDrawables;finalintN = drawables.size();floatcenter = mContentSize * .5f;if(mContentSize >0&& N >0) {// 圖像圓的半徑坝疼。finalfloatr;if(N ==1) {? ? ? ? ? ? r = mContentSize * .5f;? ? ? ? }elseif(N ==2) {? ? ? ? ? ? r = (float) (mContentSize / (2+2* Math.sin(Math.PI /4)));? ? ? ? }elseif(N ==4) {? ? ? ? ? ? r = mContentSize /4.f;? ? ? ? }else{? ? ? ? ? ? r = (float) (mContentSize / (2* (2* Math.sin(((N -2) * Math.PI) / (2* N)) +1)));finaldoublesinN = Math.sin(Math.PI / N);// 以所有圖像圓為內(nèi)切圓的圓的半徑finalfloatR = (float) (r * ((sinN +1) / sinN));? ? ? ? ? ? mOffsetY = (float) ((mContentSize - R - r * (1+1/ Math.tan(Math.PI / N))) /2f);? ? ? ? }// 初始化第一個頭像的中心位置finalfloatstartX, startY;if(N %2==0) {? ? ? ? ? ? startX = startY = r;? ? ? ? }else{? ? ? ? ? ? startX = center;? ? ? ? ? ? startY = r;? ? ? ? }// 變換矩陣finalMatrix matrix = mLayoutMatrix;// 坐標(biāo)點臨時數(shù)組finalfloat[] pointsTemp =this.mPointsTemp;? ? ? ? matrix.reset();for(inti =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) {// 以上一個圓的圓心旋轉(zhuǎn)計算得出當(dāng)前圓的圓位置matrix.postRotate(360.f / N, center, center + mOffsetY);? ? ? ? ? ? ? ? matrix.mapPoints(pointsTemp);? ? ? ? ? ? }// 取出中心點位置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è)置第一個頭像的缺口搜贤,頭像數(shù)量少于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);
其中第一行是添加一個圓形路徑桌硫,這個路徑就是布局圖中藍(lán)色圓的路徑,而第二行是設(shè)置路徑的填充模式啃炸,默認(rèn)的填充模式是填充路徑內(nèi)部铆隘,而INVERSE_WINDING模式是填充路徑外部,再配合Paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR))就可以繪制出圓形的圖像了南用。頭像上的缺口同理膀钠。(ps:關(guān)于Path.FillType和PorterDuff.Mode網(wǎng)上介紹挺多的,這里就不詳細(xì)介紹了)
下面來看一下onDraw方法:
@OverrideprotectedvoidonDraw(Canvas canvas){super.onDraw(canvas);? ? ...? ? canvas.translate(0, mOffsetY);finalPaint paint = mPaint;finalfloatgapRadius = mSteinerCircleRadius * (mGap +1f);for(inti =0; i < drawables.size(); i++) {? ? ? ? DrawableInfo drawable = drawables.get(i);? ? ? ? RectF bounds = drawable.mBounds;finalintsavedLayer = 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));// 繪制Drawabledrawable.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的一些功能筑公,如自更新雳窟、動畫、狀態(tài)等匣屡。
Drawable自更新和動畫Drawable
Drawable的自更新和動畫Drawable(如AnimationDrawable封救,AnimatedVectorDrawable等)都是依賴于Drawable.Callback接口。其定義如下:
publicinterfaceCallback{/**? ? * 當(dāng)drawable需要重新繪制時調(diào)用捣作。此時的view應(yīng)該使其自身失效(至少drawable展示部分失效)? ? *@paramwho 要求重新繪制的drawable? ? */voidinvalidateDrawable(@NonNull Drawable who);/**? ? * drawable可以通過調(diào)用該方法來安排動畫的下一幀誉结。? ? *@paramwho 要預(yù)定的drawable? ? *@paramwhat 要執(zhí)行的動作? ? *@paramwhen 執(zhí)行的時間(以毫秒為單位),基于android.os.SystemClock.uptimeMillis()? ? */voidscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what,longwhen);/**? ? * drawable可以通過調(diào)用該方法來取消先前通過scheduleDrawable(Drawable, Runnable, long)調(diào)度的動作券躁。? ? *@paramwho 要取消預(yù)定的drawable? ? *@paramwhat 要取消執(zhí)行的動作? ? */voidunscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);}
所以要支持Drawable自更新和動畫Drawable惩坑,得通過Drawable.setCallback(Drawable.Callback)方法設(shè)置Drawable.Callback接口的實現(xiàn)對象才行掉盅。好在android.view.View已經(jīng)實現(xiàn)了這個接口,在設(shè)置Drawable的時候調(diào)用一下Drawable.setCallback(MyView.this)即可以舒。但需要注意的是怔接,android.view.View實現(xiàn)Drawable.Callback接口的時候都調(diào)用了View.verifyDrawable(Drawable)以驗證需要顯示更新的Drawable是不是自己的Drawable,且其實現(xiàn)只是驗證了View自己的背景和前景:
protectedbooleanverifyDrawable(@NonNull Drawable who){// ...returnwho == mBackground || (mForegroundInfo !=null&& mForegroundInfo.mDrawable == who);}
所以只是設(shè)置了Callback的話稀轨,當(dāng)Drawable內(nèi)容改變需要重新繪制時View還是不會更新重繪的扼脐,動畫需要計劃下一幀或者取消一個計劃時也不會成功。因此我們也得驗證自己的Drawable:
privatebooleanhasSameDrawable(Drawable drawable){for(DrawableInfo d : mDrawables) {if(d.mDrawable == drawable) {returntrue;? ? ? ? }? ? }returnfalse;}@OverrideprotectedbooleanverifyDrawable(@NonNull Drawable drawable){returnhasSameDrawable(drawable) ||super.verifyDrawable(drawable);}
此時奋刽,Drawable自更新的支持和動畫Drawable的支持基本上是完成了瓦侮。當(dāng)然,View不可見和onDetachedFromWindow()時應(yīng)該是要暫陀缎常或者停止動畫的肚吏,這些在這里就不多說了,可以去看源碼(在文章結(jié)尾處有鏈接)狭魂,主要是調(diào)用Drawable.setVisible(boolean, boolean)方法罚攀。下面展示一下效果:
AnimationDrawable
狀態(tài)
一些Drawable是有狀態(tài)的,它能根據(jù)View的狀態(tài)(按下雌澄,選中斋泄,激活等)改變其顯示內(nèi)容,如StateListDrawable镐牺。要支持View狀態(tài)的話炫掐,其實只要擴(kuò)展View.drawableStateChanged()和View.jumpDrawablesToCurrentState()方法,當(dāng)View的狀態(tài)改變的時候更新Drawable的狀態(tài)就行了:
// 狀態(tài)改變時被調(diào)用@OverrideprotectedvoiddrawableStateChanged(){super.drawableStateChanged();booleaninvalidate =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();? ? }}// 這個方法主要針對狀態(tài)改變時有過渡動畫的Drawable@OverridepublicvoidjumpDrawablesToCurrentState(){super.jumpDrawablesToCurrentState();for(DrawableInfo drawable : mDrawables) {? ? ? ? drawable.mDrawable.jumpToCurrentState();? ? }}
效果:
狀態(tài)
好了睬涧,到這里控件算是完成了募胃。
其他效果展示:
效果1
效果2
源代碼:https://github.com/YiiGuxing/CompositionAvatar
我的GitHub:https://github.com/YiiGuxing
歡迎Star,謝謝畦浓!