[Android] 自定義View之仿QQ討論組頭像

轉(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,謝謝畦浓!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痹束,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子讶请,更是在濱河造成了極大的恐慌祷嘶,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秽梅,死亡現(xiàn)場離奇詭異抹蚀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)企垦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來晒来,“玉大人钞诡,你說我怎么就攤上這事。” “怎么了荧降?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵接箫,是天一觀的道長。 經(jīng)常有香客問我朵诫,道長辛友,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任剪返,我火速辦了婚禮废累,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘脱盲。我一直安慰自己邑滨,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布钱反。 她就那樣靜靜地躺著掖看,像睡著了一般。 火紅的嫁衣襯著肌膚如雪面哥。 梳的紋絲不亂的頭發(fā)上哎壳,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音尚卫,去河邊找鬼耳峦。 笑死,一個胖子當(dāng)著我的面吹牛焕毫,可吹牛的內(nèi)容都是我干的蹲坷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼邑飒,長吁一口氣:“原來是場噩夢啊……” “哼循签!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起疙咸,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤县匠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后撒轮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乞旦,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年题山,在試婚紗的時候發(fā)現(xiàn)自己被綠了兰粉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡顶瞳,死狀恐怖玖姑,靈堂內(nèi)的尸體忽然破棺而出愕秫,到底是詐尸還是另有隱情,我是刑警寧澤焰络,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布戴甩,位于F島的核電站,受9級特大地震影響闪彼,放射性物質(zhì)發(fā)生泄漏斧拍。R本人自食惡果不足惜听系,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧村象,春花似錦陶夜、人聲如沸翰意。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽流昏。三九已至扎即,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間况凉,已是汗流浹背谚鄙。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留刁绒,地道東北人闷营。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像知市,于是被迫代替她去往敵國和親傻盟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容