探一探,非常實用的GIF圖圓角控制元件(3行程式碼)

文淑發表於2019-04-08

前言

現代人的崩潰是一種默不吭聲的崩潰,看起來很正常,會說笑,會打鬧,會社交,表面平靜,實際上心裡的糟心事已經積累到一定程度了。不會摔門砸東西,不會流眼淚或歇斯心底,但可能某一秒突然就積累到極致,也不說話,也不真的崩潰,也不太想活著。也不敢去死。

這裡引用了「張帥B」的一段話,說的很貼切,我們這一代年輕人到底怎麼了?

正文

先來看看效果圖:

在這裡插入圖片描述

初步分析

眾所周知,Android 中主流的圖片載入框架有 Picasso,Glide,Fresco。Picasso 載入 gif 圖沒有動畫效果,Glide 與 Fresco 支援 gif 動效圖。Fresco 自帶的控制元件 SimpleDraweeView 支援圓角屬性,Glide 需要手動給 ImageView 設定 shape,那麼 Glide,Fresco 是否支援 gif 圖圓角?

先來看一個例子,Glide 載入 gif 圓角,先看看 xml 佈局:

    <ImageView
        android:id="@+id/iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="64dp"
        android:background="@drawable/corners_bg"
        android:src="@mipmap/gif_01"
        />
複製程式碼

圓角檔案 corners_bg :

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="8dp"></corners>
</shape>
複製程式碼

Glide 載入動圖:

    Glide.with(this).load(R.mipmap.gif_01)
            .asGif()
            .override(720, 512)
            .into(mImageView);
複製程式碼

效果圖:

在這裡插入圖片描述
發現 gif 圖並沒有顯示圓角,經過測試 Fresco 的圓角屬性,在 gif 圖上也會失效。那麼我們怎麼才能讓 gif 圖顯示圓角呢?

大家都知道 gif 是由多張靜態圖組合而成,如果處理單張圖片效率將會極其低下,既然不能對源圖片進行處理,那麼就只能在顯示控制元件上想辦法了。

經過初步分析有以下三種可行方案:

  1. 裁剪圖片控制元件
  2. 在 gif 圖片控制元件上覆蓋一層圓角圖片(4角圓角中間透明)
  3. Path 的填充樣式 FillType

方案一裁剪控制元件,改變圖片顯示區域,但裁剪的效率並不高,顧排除方案一;方案二,ui 切圓角圖片,如果在換膚的情況下,ui 需要切多套圓角圖片,可擴充套件性太差,同時覆蓋的圓角圖片載入會消耗效能,排除方案二;那就只有第三方案了,在效能與可擴充套件性方面優於前兩種方案。

Path填充樣式FillType

    /**
     * Set the path's fill type. This defines how "inside" is computed.
     *
     * @param ft The new fill type for this path
     */
    public void setFillType(FillType ft) {
        // 呼叫 c 層的 jni 方法
    }
複製程式碼

設定路徑的填充樣式,定義了 "內部" 是如何計算的。 引數 ft 是個列舉值,有 4 種型別:

    /**
     * Enum for the ways a path may be filled.
     */
    public enum FillType {
        // these must match the values in SkPath.h
        /**
         * Specifies that "inside" is computed by a non-zero sum of signed
         * edge crossings.
         */
        WINDING         (0),
        /**
         * Specifies that "inside" is computed by an odd number of edge
         * crossings.
         */
        EVEN_ODD        (1),
        /**
         * Same as {@link #WINDING}, but draws outside of the path, rather than inside.
         */
        INVERSE_WINDING (2),
        /**
         * Same as {@link #EVEN_ODD}, but draws outside of the path, rather than inside.
         */
        INVERSE_EVEN_ODD(3);
    }
複製程式碼
WINDING (0)

預設值是 WINDING (0) ,我們一起來探究下這幾個值產生的效果,先來看看下面這段程式碼:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        mPath.addCircle(600, 400, 200, Path.Direction.CW);
        mPath.addCircle(900, 400, 200, Path.Direction.CW);

        canvas.drawPath(mPath, mPaint);
        canvas.restore();
    }
複製程式碼

繪製兩相交圓,並兩圓的方向都是順時針繪製。效果圖是這樣的:

在這裡插入圖片描述
上圖是兩圓都是順時針的方向,改變一圓的繪製方向為逆時針:

    mPath.addCircle(600, 400, 200, Path.Direction.CW);
    mPath.addCircle(900, 400, 200, Path.Direction.CCW);
複製程式碼

效果圖如下:

在這裡插入圖片描述
WINDING 的原理:

其實WINDING表示非零環繞原則,從任意一點發射一條線,預設值是 0,遇到順時針交點則 +1,遇到逆時針交點則 -1,最終如果不等於 0,則認為這個點是圖形內部的點,則需要繪製顏色;反之,如果這個值是 0,則認為這個點不在圖形內部,則不需要繪製顏色。

正好解釋上圖相交的部分沒有繪製顏色,相交的部分首先是繪製順時針方向的圓 +1 ,然後繪製逆時針方向的圓 -1 ,正好等於 0,不需要繪製顏色。

EVEN_ODD (1)

這個值和 WINDING 不同,WINDING 要求每個圖形都是有方向的。EVEN_ODD 並不要求圓的方向,看看下面一段程式碼:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        mPath.setFillType(Path.FillType.EVEN_ODD);
        // 改成 Path.Direction.CCW 的效果一樣
        mPath.addCircle(600, 400, 200, Path.Direction.CW);
        mPath.addCircle(900, 400, 200, Path.Direction.CW);

        canvas.drawPath(mPath, mPaint);
        canvas.restore();
    }
複製程式碼

效果圖如下:

在這裡插入圖片描述
EVEN_ODD 原理:

英文單詞中 EVEN 是偶數,ODD 是奇數的意思。這個原則也被稱為奇偶原則。從任意一點射出一條線,與圖形的交線是奇數,則認為這個點在圖形內部,需要繪製顏色;反之如果是偶數,則認為這個點在圖形外部,不需要繪製顏色。

INVERSE_WINDING (2)

inverse 表示反轉的意思,相同的一段程式碼,繪製出來的結果是相反的。如下程式碼:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        mPath.setFillType(Path.FillType.INVERSE_WINDING);
        mPath.addCircle(600, 400, 200, Path.Direction.CW);
        mPath.addCircle(900, 400, 200, Path.Direction.CW);

        canvas.drawPath(mPath, mPaint);
        canvas.restore();
    }
複製程式碼

圓的區域都不需要繪製顏色,非圓區域繪製顏色。效果圖如下:

在這裡插入圖片描述
設定一圓的繪製方向為逆時針:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        mPath.setFillType(Path.FillType.INVERSE_WINDING);
        mPath.addCircle(600, 400, 200, Path.Direction.CW);
        mPath.addCircle(900, 400, 200, Path.Direction.CCW);

        canvas.drawPath(mPath, mPaint);
        canvas.restore();
    }
複製程式碼

兩圓相交的部分需要繪製顏色,非圓區域繪製顏色。效果圖一覽:

在這裡插入圖片描述

INVERSE_EVEN_ODD(3)

INVERSE_EVEN_ODD 模式與 INVERSE_WINDING 模式的不同的繪製方向效果一樣,程式碼如下:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        mPath.setFillType(Path.FillType.INVERSE_EVEN_ODD);
        mPath.addCircle(600, 400, 200, Path.Direction.CW);
        mPath.addCircle(900, 400, 200, Path.Direction.CCW);

        canvas.drawPath(mPath, mPaint);
        canvas.restore();
    }
複製程式碼

效果圖:

在這裡插入圖片描述
相信各位小夥伴們看到這裡,心裡很清楚了 gif 圓角控制元件的填充樣式為以下兩種情況:

  • INVERSE_WINDING 樣式,逆時針繪製圓角矩形
  • INVERSE_EVEN_ODD 樣式

編寫程式碼

通過上文原理的介紹,gif 圖圓角控制元件的程式碼就非常簡單。

起名字

接地氣的名字,能夠讓人眼前一亮,就叫 CornersGifView吧。

CornersGifView圓角控制元件

核心程式碼如下(3 行程式碼):

        mPath.reset();
        // add round rect
        mPath.setFillType(Path.FillType.INVERSE_EVEN_ODD);
        mPath.addRoundRect(new RectF(0, 0, w, h), mCorners, Path.Direction.CCW);
複製程式碼

相關引數:

  • w 表示控制元件的寬度
  • h 表示控制元件的高度
  • mCorners 圓角陣列(大小為 8,一組圓角包含長寬兩個引數,4 組圓角顧 8 個引數)
  • 繪製方向 Path.Direction.CCW 與 Path.Direction.CW 效果一致

大家可能已經注意到了,非圓角矩形區域的顏色,為 mPaint 畫筆的顏色,那麼就需要保證畫筆的顏色與父控制元件的背景顏色一致,如果父控制元件顏色為透明,那麼就需要取父控制元件的父控制元件顏色,以此類推,遞迴獲取父控制元件的顏色,請參考以下兩個方法:

    /**
     * @param vp parent view
     * @return paint color
     */
    private int getPaintColor(ViewParent vp) {
        if (null == vp) {
            return Color.TRANSPARENT;
        }

        if (vp instanceof View) {
            View parentView = (View) vp;
            int color = getViewBackgroundColor(parentView);

            if (Color.TRANSPARENT != color) {
                return color;
            } else {
                getPaintColor(parentView.getParent());
            }
        }

        return Color.TRANSPARENT;
    }
複製程式碼

通過反射獲取 View 的背景顏色值:

    /**
     * @param view
     * @return
     */
    private int getViewBackgroundColor(View view) {
        Drawable drawable = view.getBackground();

        if (null != drawable) {
            Class<Drawable> drawableClass = (Class<Drawable>) drawable.getClass();
            if (null == drawableClass) {
                return Color.TRANSPARENT;
            }

            try {
                Field field = drawableClass.getDeclaredField("mColorState");
                field.setAccessible(true);
                Object colorState = field.get(drawable);
                Class colorStateClass = colorState.getClass();
                Field colorStateField = colorStateClass.getDeclaredField("mUseColor");
                colorStateField.setAccessible(true);
                int viewColor = (int) colorStateField.get(colorState);
                if (Color.TRANSPARENT != viewColor) {
                    return viewColor;
                }
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

        return Color.TRANSPARENT;
    }
複製程式碼

不僅支援 gif 圖圓角,還支援靜態圖圓角,最終效果圖如下:

在這裡插入圖片描述

引起的思考,**FillType 能夠支援圓角矩形,那麼是不是還可以支援圓形,橢圓,任意的 Path 呢?答案是肯定的,那麼又改怎麼去實現?**這裡以擴充套件圓形為例:

新建 Path 先逆時針繪製控制元件大小的矩形,再繪製圓形(以控制元件中心為圓點),接著設定 FillType 為 WINDING ,最後新增路徑到原始路徑上 addPath ,程式碼就像這樣:

    private void addCirclePath() {
        int w = getWidth();
        int h = getHeight();

        Path addPath = new Path();
        addPath.addRect(new RectF(0, 0, w, h), Path.Direction.CCW);
        addPath.addCircle(w / 2, h / 2, Math.min(w, h) / 2, Path.Direction.CW);
        setPath(addPath);
    }
複製程式碼
    private void setPath(Path path) {
        mPath.reset();

        mPath.setFillType(Path.FillType.WINDING);
        mPath.addPath(path);

        invalidate();
    }
複製程式碼

效果圖如下:

在這裡插入圖片描述

結束語

「張帥B」的那段話,概述的非常經典。都說 80 後的在忙著掙錢;00 後在忙著談戀愛;只有我們 90 的在忙著賺錢又談戀愛,最後錢沒掙到,戀愛也沒談到。

最後再囉嗦一點,生活都不容易,一直堅持做一件事情更不容易,還希望各位朋友,能夠多多支援,小編新開的公眾號「控制元件人生」,有你們的相伴,才有寫下去的動力。小編還會不定期發放現金紅包,發放現金紅包,發放現金紅包。

原始碼地址:

github.com/HpWens/MeiW…

qrcode_for_gh_232b5a56667d_258.jpg

掃一掃 關注我的公眾號
與小夥伴們一同成長~

相關文章