自定義View實用小技巧

1.不用傳入Context參數的DP轉PX,在安卓中進行繪製最後顯示都是以PX爲單位的,因此咱們通常須要用將設計圖上的DP轉爲PX。

public static float dp2px(float dp) {
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}
複製代碼

2.三角函數獲取座標值 通用代碼

咱們在繪製自定義View的過程當中不可避免的常常會接觸到三角函數,現提供一個通用的獲取X,Y點的代碼, 要注意的是咱們繪製的時候0°是三點鐘方向而非傳統認知的12點鐘方向,畢竟View的座標系默認是以左上角爲原點的。 android

在這裏插入圖片描述
據圖所示咱們最終的X,Y點其實就是(cos * 半徑,sin * 半徑),化爲代碼就爲:

float cos = (float) Math.cos(Math.toRadians(angle));
float sin = (float) Math.sin(Math.toRadians(angle));
複製代碼

這裏注意咱們的角度都以默認0°爲起始點進行相加,若是畫線的話則爲:canvas

canvas.drawLine(getWidth()/2,getHeight() / 2,
        (float) Math.cos(Math.toRadians(angle)) * RADIUS,  //RADIUS爲半徑,angle爲角度,圖示角度爲90+90+90+60=240
        (float) Math.sin(Math.toRadians(angle)) * RADIUS,
        paint);
複製代碼

3.Xfermode的使用

能夠運用Xfermode繪製出多種重疊,交集等效果 如圖: app

在這裏插入圖片描述

public class XfermodeView extends View {
    private static final float RADIUS = DisplayUtil.dp2px(100);
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    Bitmap bitmap;

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

    private void init() {
        savedArea.set(RADIUS, RADIUS, RADIUS * 2, RADIUS * 2);
        bitmap = getBitmap((int) RADIUS * 2);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setColor(Color.parseColor("#3F51B5"));
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, RADIUS, paint);
        canvas.drawBitmap(bitmap, getWidth() / 2, getHeight() / 2, paint);
    }

    Bitmap getBitmap(int width) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
        options.inJustDecodeBounds = false;
        options.inDensity = options.outWidth;
        options.inTargetDensity = width;
        return BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
    }
}
複製代碼

使用Xfermode以後的效果: ide

在這裏插入圖片描述

public class XfermodeView extends View {
    private static final float RADIUS = DisplayUtil.dp2px(100);
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    RectF savedArea = new RectF();
    Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
    Bitmap bitmap;


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


    private void init() {
        savedArea.set(RADIUS, RADIUS, RADIUS * 2, RADIUS * 2);
        bitmap = getBitmap((int) RADIUS * 2);
    }


    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setColor(Color.parseColor("#3F51B5"));
        int saved = canvas.saveLayer(null,null, Canvas.ALL_SAVE_FLAG);//保存狀態 開啓離屏緩衝
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, RADIUS, paint);//DST
        paint.setXfermode(xfermode);
        canvas.drawBitmap(bitmap, getWidth() / 2, getHeight() / 2, paint);//SRC
        paint.setXfermode(null);
        canvas.restoreToCount(saved);
    }


    Bitmap getBitmap(int width) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
        options.inJustDecodeBounds = false;
        options.inDensity = options.outWidth;
        options.inTargetDensity = width;
        return BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
    }
}
複製代碼

能夠看到模式設置爲了SRC_IN,則最終效果爲取SRC和DST的交集同時顯示SRC的交集部分函數

  • 使用Xfermode須要注意的點:繪製以前使用離屏緩衝保存畫布狀態,繪製以後還原。開啓離屏緩衝的緣由是若是不開啓那麼是Xfermode是沒效果的,由於默認的DST蒙版將會被認爲是整個View。性能

    int saved = canvas.saveLayer(null,null, Canvas.ALL_SAVE_FLAG);//保存狀態 開啓離屏緩衝
    ...
    canvas.restoreToCount(saved);
    複製代碼
  • 設置離屏緩衝時還能夠指定裁取的大小,防止性能的浪費。ui

    RectF savedArea = new RectF();
    savedArea.set(left, top, right, bottom);
    int saved = canvas.saveLayer(savedArea, paint);
    複製代碼

Tips設置setXfermode以前繪製的是DST,後繪製的是SRC(圖例SRC是圖片,DST是繪製的圓形,採用SRC_IN,最終取交集而且顯示SRC圖片的內容),具體的效果圖能夠參考下圖:spa

在這裏插入圖片描述

4.文字的繪製

中心點的肯定:設計

須要注意的是文字的X,Y起始點並非左上角而是左下角。3d

canvas.drawText("abcd", getWidth() / 2, getHeight() / 2 , paint);
複製代碼

咱們能夠經過:

paint.setTextAlign(Paint.Align.CENTER);
複製代碼

來設置文字的中心點,如此設置以後X的起始點就爲你所定義的位置了。 可是繪製事後文字是會偏上的,由於默認的點爲BaseLine,咱們須要將文字下移偏移量纔是一個真正的中點值。

在這裏插入圖片描述

paint.setTextSize(DisplayUtil.dp2px(50));
paint.setStyle(Paint.Style.FILL);
paint.setTextAlign(Paint.Align.CENTER);
paint.getTextBounds("abcd", 0, "abcd".length(), rect);
float offset = (rect.top + rect.bottom) / 2;
canvas.drawText("abcd", getWidth() / 2, getHeight() / 2 - offset, paint);
複製代碼

這樣減去偏移量文字將會下移爲真正的中點,可是注意這種方法是基於BaseLine的,因此當文字肯定不會改變的時候用這種方式比較合適。

在這裏插入圖片描述

會改變的文字用:

Paint.FontMetrics fontMetrics = new Paint.FontMetrics();

paint.getFontMetrics(fontMetrics);
paint.setTextSize(DisplayUtil.dp2px(100));
paint.setStyle(Paint.Style.FILL);
paint.setTextAlign(Paint.Align.CENTER);
float offset = (fontMetrics.ascent + fontMetrics.descent) / 2;
canvas.drawText("abcd", getWidth() / 2, getHeight() / 2 - offset, paint);
複製代碼

這種方式不會隨着文字的BaseLine而改變,防止由於文字改變可能出現的跳躍問題。

5.文字的左對齊

須要減去左邊的文字默認間距,以下:

// 繪製文字左對齊
paint.setTextAlign(Paint.Align.LEFT);
Rect rect = new Rect();
paint.getTextBounds("abcd", 0, "abcd".length(), rect);
canvas.drawText("abcd", 0 - rect.left, 300, paint);
複製代碼

6.文字的多行繪製

  • 若是是僅僅多行繪製那麼很是簡單,直接使用StaticLayout就能夠了:

    {
        staticLayout = new StaticLayout(text, textPaint, 600, Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
         //StaticLayout 並非一個 View 或者 ViewGroup ,而是 android.text.Layout 的子類,
         // 它是純粹用來繪製文字的。 StaticLayout 支持換行,它既能夠爲文字設置寬度上限來讓文字自動換行,也會在 \\n 處主動換行。
         //參數說明
         //width 是文字區域的寬度,文字到達這個寬度後就會自動換行;
         //align 是文字的對齊方向;
         //spacingmult 是行間距的倍數,一般狀況下填 1 就好;
         //spacingadd 是行間距的額外增長值,一般狀況下填 0 就好;
         //includeadd 是指是否在文字上下添加額外的空間,來避免某些太高的字符的繪製出現越界。
     }    
     @Override
     protected void onDraw(Canvas canvas) {
         super.onDraw(canvas);
         // 使用 StaticLayout 代替 Canvas.drawText() 來繪製文字,
         // 以繪製出帶有換行的文字
         canvas.save();
         canvas.translate(50, 40);
         staticLayout.draw(canvas);
         canvas.restore();
     }
    複製代碼
  • 文字的精確折行

通常用於跟圖片相交的需求使用: 主要是兩個API的使用:

  1. paint.breakText();
  2. canvas.drawText();
int count = paint.breakText(text, start, length, true, maxWidth, cutWidth);
//截取字符串。
//參數爲繪製的文字,開始字符截取的字符,終止字符,是否順時,截取的寬度,保存截取的寬度
canvas.drawText(text, start, start + count, 0, verticalOffset, paint);
//第二和第三個參數爲字符串的起始點和終止點
複製代碼

咱們的中心思想就是每次用breakText計算出當次的起點文字到終點文字的長度,根據長度計算出文字的終止點是哪裏,而後用drawText根據起止點和終止點截取文字並繪製。 下邊以一個實例來講明,上代碼:

在這裏插入圖片描述

public class ImageTextView extends View {
        private static final float IMAGE_WIDTH = DisplayUtil.dp2px(120);
        private static final float IMAGE_Y = DisplayUtil.dp2px(50);
        private boolean isLeft = true;
        private boolean isInImage = false;
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        Bitmap bitmap;
        Paint.FontMetrics fontMetrics = new Paint.FontMetrics();
        String text = "This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text.";
        float[] cutWidth = new float[1];
    
        public ImageTextView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }

    private void init() {
        bitmap = getAvatar((int) IMAGE_WIDTH);
        paint.setTextSize(DisplayUtil.dp2px(14));
        paint.getFontMetrics(fontMetrics);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //繪製文字
        canvas.drawBitmap(bitmap, getWidth() / 2 - IMAGE_WIDTH / 2, IMAGE_Y, paint);

        int length = text.length();
        float verticalOffset = -fontMetrics.top;

        for (int start = 0; start < length; ) {
            int maxWidth;
            float textTop = verticalOffset + fontMetrics.top;
            float textBottom = verticalOffset + fontMetrics.bottom;
             //判斷是否在圖片區域內
            if (textTop > IMAGE_Y && textTop < IMAGE_Y + IMAGE_WIDTH 
                    || textBottom > IMAGE_Y && textBottom < IMAGE_Y + IMAGE_WIDTH) {
                // 文字和圖片在同一行,減去圖片的寬度
                isInImage = true;
                maxWidth = (int) (getWidth() / 2 - IMAGE_WIDTH / 2);
            } else {
                isInImage = false;
                // 文字和圖片不在同一行
                maxWidth = getWidth();
            }
            int count = paint.breakText(text, start, length, true, maxWidth, cutWidth);
            if (isInImage) {//若是是圖片顯示區域內
                if (isLeft) { //在圖片左邊
                    isLeft = false;
                    canvas.drawText(text, start, start + count, 0, verticalOffset, paint);
                } else { //在圖片右邊
                    isLeft = true;
                    canvas.drawText(text, start, start + count, getWidth() / 2 + IMAGE_WIDTH / 2, verticalOffset, paint);
                    verticalOffset += paint.getFontSpacing();  //再右邊才換行
                }
            } else {
                canvas.drawText(text, start, start + count, 0, verticalOffset, paint);
                verticalOffset += paint.getFontSpacing(); //換行
            }
            start += count;
        }
    }

    Bitmap getAvatar(int width) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
        options.inJustDecodeBounds = false;
        options.inDensity = options.outWidth;
        options.inTargetDensity = width;
        return BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
    }
}
複製代碼

如上,此例咱們整體的思路就是判斷當前文字是否在圖片的顯示高度之類,若是在圖片高度範圍以內則作截取字符的處理,在判斷是否超太高度的時候能夠根據本身的邏輯來設置,這裏只是提供一種思路,實際狀況能夠根據本身的需求計算處理。

7.canvas的裁剪和變換

canvas的裁剪主要有4個API:

  1. canvas.clipRect();
  2. canvas.clipPath();
  3. canvas.clipOutPath();
  4. canvas.clipOutRect();

當進行裁剪以後你繪製的部分只能是在你繪製的部分中被顯示出來,以下所示:

在這裏插入圖片描述

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.clipRect(0, 0, 100, 100);
    canvas.drawBitmap(bitmap, 0, 0, paint);
}
複製代碼

能夠看到只繪製了被切割矩形的部分。

Tips:這裏還須要注意的一點是當進行了clipPath操做以後畫筆的抗鋸齒效果就會無效了,畫出的東西頗有多是帶有毛邊的,好比用clipPath切割一個圓形而後繪製一個圓形的頭像,在這種狀況下就能夠考慮Xfermode而非clipPath了。

canvas的變換:

  1. canvas.rotate(degree);

  2. canvas.translate(x,y);

  3. canvas.scale(x,y);

  4. canvas.skew(x,y);

這裏只須要注意一點,canvas的變換是改變的座標系起始點也就是左邊系的原點,好比當我調用tranlate以後在調用rotate的狀況是這樣的:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    bitmap = getAvatar(100);
    canvas.translate(200,200);
    canvas.rotate(45);
    canvas.drawBitmap(bitmap, 0, 0, paint);
}
複製代碼

在這裏插入圖片描述
能夠看到最後繪製是在0,0點繪製的bitmap圖片,也就是說咱們每次對座標系的操做其實都是操做的左邊系的原點。

Tips:須要注意的一點是一般進行canvas的變換或裁剪以前都須要調用canvas.save()去保存canvas狀態,繪製完成以後調用canvas.restore()去還原canvas的座標,若是不還原的話以後的繪製都會以你改變後的原點爲基礎進行繪製。

相關文章
相關標籤/搜索