賣一下廣告,歡迎你們關注個人微信公衆號,掃一掃下方二維碼或搜索微信號 stormjun94(徐公碼字),便可關注。 目前專一於 Android 開發,主要分享 Android開發相關知識和一些相關的優秀文章,包括我的總結,職場經驗等。android
前言
在上一篇博客 Android 圓形頭像的兩種實現方式 中,咱們塔倫了實現圓形頭像的兩種實現方式。git
- 第一種: 使用 Paint 的 Xfermode 實戰
- 第二種: 使用 BitmapShader 實現
今天,讓咱們一塊兒來看一下怎樣實現正 N 變形圓角頭像的實現。github
在講解以前,讓咱們先來看一下怎樣使用咱們的控件
老規矩,在講解怎樣實現之前,咱們先一塊兒來看一下怎樣使用咱們的自定義控件。canvas
自定義屬性說明
<attr name="type"> <enum name="circle" value="0" /> <enum name="round" value="1" /> <enum name="polygon" value="2" /> </attr> <declare-styleable name="MultiImageView"> <attr name="type" /> <attr name="miv_border_width" format="dimension" /> <attr name="miv_border_color" format="color" /> <attr name="miv_border_overlay" format="boolean" /> <attr name="miv_fill_color" format="color" /> <attr name="miv_corner_radius" format="dimension" /> <attr name="miv_sides" format="integer" /> <attr name="miv_rotate_angle" format="float" /> </declare-styleable>
參數 | 說明 |
---|---|
type | 相應的值有 circle,round,polygon |
miv_border_width | 表示邊界 Path 的寬度 (默認值是 0 ) |
miv_border_color | 表示邊界 Path 的 Color |
miv_border_overlay | 表示邊界 Path 是否要覆蓋在圖片上面 |
miv_fill_color | 表示填充圓的顏色,默認是 Translate,即不可見 |
miv_corner_radius | 只有當 type round 或者 polygon 的時候才生效,表示邊界 Path 圓角半徑的大小, |
miv_sides | 正 N 邊形的變數,只有 type 爲 polygon 的時候,該屬性才生效 |
miv_rotate_angle | 旋轉的角度,只有 type 爲 polygon 的時候,該屬性才生效 |
指定圓形頭像
<com.xj.shapeview.multiimageview android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/tanyan" app:type="circle" />
指定圓角矩形
<com.xj.shapeview.multiimageview android:layout_marginLeft="15dp" android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/tanyan" app:type="round" app:miv_corner_radius="15dp" />
指定正 N 邊形
正五邊形微信
<com.xj.shapeview.multiimageview android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/tanyan" app:type="polygon" app:miv_sides="5" app:miv_corner_radius="25dp" />
若是須要其旋轉相應的角度,咱們只需指定 app:miv_rotate_angle="180" 便可,這裏以 180 度爲列子講解說明app
若是須要正六邊形,只須要更改成 app:miv_sides="6"ide
效果圖佈局
相應的佈局文件實現post
<!--?xml version="1.0" encoding="utf-8"?--> <scrollview xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <gridlayout android:columncount="3" android:rowcount="3" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.xj.shapeview.multiimageview android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/tanyan" app:type="circle" app:miv_sides="6" app:miv_corner_radius="15dp" /> <com.xj.shapeview.multiimageview android:layout_marginLeft="15dp" android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/tanyan" app:miv_sides="5" app:type="round" app:miv_corner_radius="15dp" /> <com.xj.shapeview.multiimageview android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/tanyan" app:type="polygon" app:miv_sides="5" app:miv_corner_radius="25dp" /> <com.xj.shapeview.multiimageview android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/tanyan" app:type="polygon" app:miv_sides="5" app:miv_corner_radius="25dp" app:miv_rotate_angle="180" /> <com.xj.shapeview.multiimageview android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/tanyan" app:miv_sides="7" app:type="polygon" app:miv_corner_radius="0dp" app:miv_rotate_angle="0" /> <com.xj.shapeview.multiimageview android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/tanyan" app:miv_sides="6" app:type="polygon" app:miv_corner_radius="0dp" app:miv_border_overlay="true" app:miv_fill_color="@color/colorAccent" /> <com.xj.shapeview.multiimageview android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/tanyan" app:miv_sides="6" app:type="polygon" app:miv_corner_radius="0dp" app:miv_rotate_angle="0" app:miv_border_overlay="true" app:miv_border_width="1dp" app:miv_border_color="@android:color/darker_gray" /> <com.xj.shapeview.multiimageview android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/tanyan" app:miv_sides="6" app:type="polygon" app:miv_corner_radius="0dp" app:miv_rotate_angle="0" app:miv_border_overlay="false" app:miv_border_width="1dp" app:miv_border_color="@android:color/black" /> <com.xj.shapeview.multiimageview android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/tanyan" app:miv_sides="7" app:type="polygon" app:miv_corner_radius="10dp" app:miv_rotate_angle="0" /> </gridlayout> </scrollview>
正 N 邊形圓角頭像的實現原理分析
要實現正 N 變形主要有幾個難點.net
- 怎樣讓咱們的頭像變成正 N 邊形
- 怎樣繪製正 N 邊形
- 怎樣繪製帶圓角的正 N 邊形
怎樣讓咱們的頭像變成正 N 邊形?
其實這個問題在上篇博客已經講到,有兩種實現方式。
- 第一種: 使用 Paint 的 Xfermode 實戰
- 第二種: 使用 BitmapShader 實現
今天,這邊博客主要以 BitmapShader 爲例子實現。
核心代碼實現
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); mBitmapPaint.setAntiAlias(true); mBitmapPaint.setShader(mBitmapShader); @Override protected void onDraw(Canvas canvas) { ----- Path path = getPath(canvas,mType,(int)mDrawableRadius*2,(int)mDrawableRadius*2,mDrawableRadius,mSides,mCornerRadius); canvas.drawPath(path,mBitmapPaint); }
核心思路分析:
- 拿到 Bitmap,並使用 BitmapShader 進行包裝
- 將 mBitmapShader 設置給畫筆 Paint
- 第三步,在 onDraw 方法,將其繪製出來
怎樣繪製正 N 邊形
這裏的思想主要來自該博客 如何用Canvas畫一個正多邊形
數學原理分析
首先,咱們先來看一張圖片
從圖中能夠看一看到,咱們若想繪製出一個正 N 邊形,那麼咱們只須要計算出各個點的座標,而後使用 Path 鏈接起來便可。
那咱們要怎樣計算出各個點的座標呢
- 從圖中不可貴出,圓心角 a 的度數爲 360/n,弧度計算爲 2π/n
- 若是把圓心的座標爲(0,0),那麼頂點P1的座標爲[X1=cos(a),Y1=sin(a)]。
- 以此類推,頂點Pn座標爲[Xn=cos(an),Yn=sin(an)]。 圓心的實際座標是外接矩形的中心:[Ox=(rect.right+rect.left)/2 , Oy=(rect.top+rect.bottom)/2]。 因此Pn的實際座標是[Xn+Ox,Yn+Oy]。
最後咱們把把 P0-P1…Pn 連起來,就是咱們要的結果了。
核心僞代碼實現
float a = 2π / n ; // 角度 Path path = new Path(); for( int i = 0; i < = n; i++ ){ float x = R * cos(a * i); float y = R * sin(a * i); if (i = 0){ path.moveTo(x,y); // 移動到第一個頂點 }else{ path.lineTo(x,y); // } } drawPath(path);
實際代碼實現
在上面的例子中,咱們假設咱們的圓形座標是 (0,0), 但實際上並非,實際上在 Android 中咱們的圓心座標是 (width/2,height/2)。所以,咱們在計算座標的時候須要加上
圓心座標 float mX = (rect.right + rect.left) / 2; float my = (rect.top + rect.bottom) / 2; // PN點的 x,y 座標 float nextX = mX + Double.valueOf(r * Math.cos(alpha)).floatValue(); float nextY = my + Double.valueOf(r * Math.sin(alpha)).floatValue();
固然咱們這裏以能夠用 canvas 的 translate 方法來移動。
public static void drawPolygon (RectF rect, Canvas canvas, Paint paintByLevel, int number) { if(number < 3) { return; } float r = (rect.right - rect.left) / 2; float mX = (rect.right + rect.left) / 2; float my = (rect.top + rect.bottom) / 2; Path path = new Path(); for (int i = 0; i <= number; i++) { // - 0.5 : Turn 90 ° counterclockwise float alpha = Double.valueOf(((2f / number) * i - 0.5) * Math.PI).floatValue(); float nextX = mX + Double.valueOf(r * Math.cos(alpha)).floatValue(); float nextY = my + Double.valueOf(r * Math.sin(alpha)).floatValue(); if (i == 0) { path.moveTo(nextX, nextY); } else { path.lineTo(nextX, nextY); } } canvas.drawPath(path, paintByLevel); }
怎樣繪製帶有圓角的正 N 邊形
這個問題我一開始的思路是根據圓形的半徑,而後計算出各個點的座標,接着使用 path 中的 addArc() 方法來繪製。可是在計算各個點的座標的時候,遇到不少難度,最後沒法得出。
後面查閱了 Android 官方的文檔,發現了有這樣一個方法
> PathEffect setPathEffect (PathEffect effect)
從字面意思很容易理解,就是設置 PathEffect,能夠對 Path 產生相應的影響。
那這個 PathEffect 又是什麼東東呢?
> public class PathEffect extends Object
> Known Direct Subclasses ComposePathEffect,CornerPathEffect,DashPathEffect,DiscretePathEffect,PathDashPathEffect,SumPathEffect
從官方文檔能夠了解到是繼承於 Object 的,實現的子類有 ComposePathEffect, CornerPathEffect, DashPathEffect 等。
看到這裏的時候你有沒有忽然有一種醍醐灌頂的感受? 這個 CornerPathEffect 是否是就能夠實現呢?沒錯,確實能夠實現,並且賊簡單。
核心代碼只有這幾句,就可讓咱們繪製出的正 N 邊形具備圓角
CornerPathEffect cornerPathEffect = new CornerPathEffect(mCornerRadius); mBitmapPaint.setPathEffect(cornerPathEffect);
代碼實現細節注意事項
當空間的寬度和高度不一致的時候,半徑怎樣取值?
這裏咱們選擇寬度和高度值較小的一個,而後除以2
mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);
當圖片較大的時候,會不會發生 OOM
當圖片較大的時候,咱們會對其進行相應的縮放,採用的是矩陣的方法
private void updateShaderMatrix() { float scale; float dx = 0; float dy = 0; mShaderMatrix.set(null); if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) { scale = mDrawableRect.height() / (float) mBitmapHeight; dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f; } else { scale = mDrawableRect.width() / (float) mBitmapWidth; dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f; } mShaderMatrix.setScale(scale, scale); mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top); mBitmapShader.setLocalMatrix(mShaderMatrix); }
自定義控件怎樣支持 padding 屬性
在繪製圖片的時候,咱們對其進行相應的處理,確保咱們的座標是正確的。
float left = getPaddingLeft() + (availableWidth - sideLength) / 2f; float top = getPaddingTop() + (availableHeight - sideLength) / 2f;
case CIRCLY: ClipHelper.setCirclePath(path,width,height); break; case RECTAHGE: ClipHelper.setRectangle(path,calculateBounds(),cornerRadius); break; case POLYGON: ClipHelper.setPolygon(path,calculateBounds(),sides,mRotateAngles); break; private RectF calculateBounds() { int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight(); int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom(); int sideLength = Math.min(availableWidth, availableHeight); float left = getPaddingLeft() + (availableWidth - sideLength) / 2f; float top = getPaddingTop() + (availableHeight - sideLength) / 2f; return new RectF(left, top, left + sideLength, top + sideLength); }
正 N 邊形的角度旋轉是怎樣實現的。
其實,這裏,咱們採用的是矩陣的方式進行旋轉的,調用 path.transform 方法
Matrix matrix = new Matrix(); matrix.postRotate(rotateAngle,mX,my); path.transform(matrix);
題外話
在開發的時候,一剛開始說要實現圓角六邊形的時候,查閱了相關的資料,知道有兩種方法
- 第一種方法,讓 UI 設計師直接給圖, 使用 Paint 的 Xfermode 實現
- 第二種方法:直接繪製 Path;
那時候項目比較趕,採用的是第一種方式實現。不過做爲一名程序猿,感受採用第一種方法實現,總感受有點 low。後面晚上下班的時候,查閱了相關的資料,最終終於實現了上述的效果。
這種正 N 邊形圓角頭像的效果,說難也不難,說容易也不容易。由於裏面綜合了不少知識點,須要一步步去處理。(好比怎樣繪製正 N 邊形,怎樣支持圓角,怎樣處理 Padding 等等)。
最後,給你們推薦 github 上面的一個開源庫。ShapeOfView,裏面實現了不少常見的圖片(心形,五角星。六角形等)
參考博客:如何用Canvas畫一個正多邊形
Android 圓形頭像的兩種實現方式 Android 正 N 邊形圓角頭像的實現
若是,你以爲效果還不錯,請到個人 github 上面 star,謝謝。