Android自定義探照燈效果的頭像

簡介

  • WanAndroid項目中涉及到用戶頭像的生成,需求有以下幾點:
    • 仿照探照燈效果
    • 頭像含有用戶姓名

複習View繪製流程

Android窗口機制

在這裏插入圖片描述

  • PhoneWindow:繼承自Window類,負責管理界面顯示以及事件響應,每個Activity 界面都包含一個PhoneWindow對象,它是Activity和整個View系統交互的接口。

  • DecorView:是PhoneWindow中的起始節點View,繼承自View類,作爲整個視圖容器來使用的,主要負責設置窗口屬性。

  • ViewRoot:在系統啓動一個Activty組件的同時將其創建,類似於MVC模型中的Controller,負責管理、佈局和渲染窗口UI等事務。

View繪製準備

在這裏插入圖片描述

  • 系統啓動一個Activity的同時創建一個ViewRoot實例。Activity在attach階段生成一個PhoneWindow對象,它包含一個DecorView對象。在Activity執行onCreate中的setContentView之後,將設置進來的view加載進入ContentViews區域。之後觸發ViewRoot中的scheduleTraversals異步函數,從而進入ViewRoot的performTraversals函數,View的繪製從這裏開始。
View繪製流程

在這裏插入圖片描述

  • View 的繪製流程是從ViewRoot 的 performTraversals 方法開始,它以DecorView爲父容器開始自上而下的進行View繪製工作,它經過 measure 、 layout 和 draw 三個過程才能最終將一個 View 繪製出來,其中 measure 用來測量 View 的寬和高,layout 用來確定 View 在父容器中的放置位置,而 draw 則負責將 View 繪製在屏幕上
  • 注意
    • Measure 過程決定了 View 的寬/高, Measure 完成以後可以通過 getMeasuredWidthgetMeasuredHeight 方法來獲取到 View 測量後的寬/高,在幾乎所有的情況下它都等同於 View 最終的寬/高,但是特殊情況除外。
    • Layout 過程 決定了 View 的四個頂點的座標和實際的 View 的寬/高,完成以後可以通過getTop、getBottom、getLeft、getRight 來拿到 View 的四個頂點的位置,並可以通過getWidth 和 getHeight 方法拿到 View 最終的寬/高。
    • Draw 過程則決定了 View 的顯示,只有 draw 方法完成以後 View 的內容才能呈現在屏幕上。

探照燈效果

具體實現

發光效果
  • 繪製發光效果難度不大直接上代碼
// 繪製發光效果
int color = getColor(R.color.always_white_text);
mPaintBackground.setColor(color);
mPaintBackground.setStyle(Paint.Style.FILL);
mPaintBackground.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.SOLID));
canvas.drawCircle(getWidth() / 2, getWidth() / 2, (getWidth() - 20) / 2, mPaintBackground);

繪製頭像文本

難點:垂直居中
  • 首先文字的繪製 參考【繪圖入門

  • 在Android中繪製文字採用基線的方式,同時系統還會利用四條輔助線來輔助繪製文字,具體釋義如下 :
    在這裏插入圖片描述

  • 注意:

    • 藍線:基線
    • 紅線:ascent線
    • 綠線:descent線
    • 黃線:top線
    • 黑線:bottom線
ascent:系統推薦的,在繪製單個字符的時,字符應當的最高高度所在線
descent:系統推薦的,在繪製單個字符時,字符應當的最低高度所在線
top:可繪製的最高高度所在線
bottom:可繪製的最低高度所在線

我們就儘量將文字繪製在系統推薦的最高和最低的限度之內,這樣在不同的屏幕的手機上,文字都可以完整的展現出來

// 通過如下的方式計算出顯示文字的區域
ascent線y座標 = baseline線y座標 + fontMetric.ascent
descent線 y座標 = baseline線的y座標 + fontMetric.descent
top線y座標 = baseline線y座標 + fontMetric.top
bottom線的y座標 = baseline線y座標 + fontMetric.bottom
  • 明白瞭如何繪製就來繪製垂直居中的文字吧
  • 公式:
 int baseLine = getHeight() / 2 - fontMetricsInt.descent  + (fontMetricsInt.bottom - fontMetricsInt.top) / 2;
對公式的理解
  • getHeight() / 2 - fontMetricsInt.descent
    • 將文本的bottom線擡高至控件的1/2處,效果圖及數據如下
    • 在這裏插入圖片描述
getHeight() / 2: 270
baseLine:       252
ascent:        185.20312
descent:      269.57812
top:        175.95703
bottom:     271.51172

getHeight () /2 約等於desect 也約等於bottom, 即控件向上擡至控件1/2處
  • getHeight() / 2 - fontMetricsInt.descent + (fontMetricsInt.bottom - fontMetricsInt.top) / 2
    • 即將文字繪製在控件中央,文本的輔助線(top+bottom)/2就是文本的中位線(我是這樣理解的)恰好在控件中位線處,即垂直居中。效果圖及數據如下
    • 在這裏插入圖片描述
getHeight() / 2:  270
baseLine:          300
ascent:           233.20312
descent:           317.57812
top:              223.95703
bottom:          319.51172

getHeight() / 2 - fontMetricsInt.descent  + 
(fontMetricsInt.bottom - fontMetricsInt.top) / 2;

270 約等於現在的(top+bottom)/2, 即將文字繪製在控件中央
完整代碼
@SuppressLint("AppCompatCustomView")
public class CustomUserAvatar extends ImageView {


    private Paint mPaintText;


    private Paint mPaintBackground;
    
    private Rect mRect;


    private String mUserName;


    public CustomUserAvatar(Context context) {
        super(context);
        init();
    }


    public CustomUserAvatar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }


    public CustomUserAvatar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }


    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintBackground = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintBound = new Paint(Paint.ANTI_ALIAS_FLAG);
        mRect = new Rect();
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, widthMeasureSpec);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 繪製發光效果
        int color = getColor(R.color.always_white_text);
        mPaintBackground.setColor(color);
        mPaintBackground.setStyle(Paint.Style.FILL);
        mPaintBackground.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.SOLID));
        canvas.drawCircle(getWidth() / 2, getWidth() / 2, (getWidth() - 20) / 2, mPaintBackground);


        // 設置文本大小
        mPaintText.setTextSize(getWidth() / 3);
        // 設置文本顏色跟隨應用主題顏色
        mPaintText.setColor(Constant.getColor(getContext()));
        // 設置畫筆粗細
        mPaintText.setStrokeWidth(5);
        // 設置陰影半徑
        mPaintText.setShadowLayer(5, 5, 5, getColor(R.color.black));
        // 繪製文字的最小矩形
        mPaintText.getTextBounds(mUserName, 0, 1, mRect);
        Paint.FontMetricsInt fontMetricsInt = mPaintText.getFontMetricsInt();
        // baseLine上面是負值,下面是正值
        // 所以getHeight()/2-fontMetricsInt.descent 將文本的bottom線擡高至控件的1/2處
        // + (fontMetricsInt.bottom - fontMetricsInt.top) / 2:(fontMetricsInt.bottom - fontMetricsInt.top) 文本的輔助線(top+bottom)/2就是文本的中位線(我是這樣理解的)恰好在控件中位線處
        int baseLine = getHeight() / 2 - fontMetricsInt.descent + (fontMetricsInt.bottom - fontMetricsInt.top) / 2;
        // 水平居中
        mPaintText.setTextAlign(Paint.Align.CENTER);
        canvas.drawText(mUserName, getWidth() / 2, baseLine, mPaintText);
    }




    /**
     * 判斷一個字符是否是中文
     */
    public boolean isChineseChar(char c) {
        // 根據字節碼判斷
        return c >= 0x4E00 && c <= 0x9FA5;
    }


    /**
     * 判斷一個字符串是否含有中文
     *
     * @param str
     * @return
     */
    public boolean isChineseString(String str) {
        if (str == null) {
            return false;
        }
        for (char c : str.toCharArray()) {
            if (isChineseChar(c)) {
                return true;
            }
        }
        return false;
    }


    /**
     * 設置顯示的名字
     *
     * @param userName
     */
    public void setUserName(String userName) {
        // 中文名字取後兩個
        if (isChineseString(userName)) {
            if (userName.length() > 2) {
                mUserName = userName.substring(userName.length() - 2, userName.length());
            } else {
                mUserName = userName;
            }
        } else {
            // 非中文名字取第一個
            if (userName.length() > 1) {
                mUserName = userName.substring(0, 1);
                mUserName = mUserName.toUpperCase();
            } else {
                mUserName = userName;
                mUserName = mUserName.toUpperCase();
            }
        }
        invalidate();
    }
}



發佈了144 篇原創文章 · 獲贊 54 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章