Andorid顯示圓形圖片的4種方式

這篇博客主要講解了Android實現圓形圖片的4種方式。android

Android中並無一個原生的控件,能夠顯示圓形或圓角圖片,所以須要咱們本身去定義這樣一個控件。canvas

實現圓形/圓角圖片的核心思想,就是按照必定形狀切割/繪製咱們的原始控件,大概有如下4種方法:segmentfault

  • 利用canvas.clipPath方法,按照自定義的Path圖形去切割控件
  • 利用canvas.setBitmapShader,按照自定義的BitmapShader去從新繪製控件
  • 利用view.setOutlineProvider/setClipToOutline,按照自定義的Outline去切割控件
  • 利用Glide的Transformation變換,顯示圓形圖片

關於ImageView的幾個知識點:app

  • ImageView顯示圖片,底層是經過Canvas將咱們的圖片資源畫到View控件上實現的;
    所以,要讓其顯示圓形圖片,只須要對Canvas進行相應的變化,好比切割圓形、繪製圓形。
  • 編寫自定義控件時,要繼承AppCompatImageView,而不是ImageView,
    由於AppCompatImageView擁有ImageView沒有的功能,好比Tinting

尊重原創,轉載請註明出處 https://segmentfault.com/a/11...
本文出自 強哥大天才的博客框架

Path切割

思路ide

咱們能夠定義一個圓形Path路徑,而後調用canvas.clipPath,將圖片切割成圓形ui

缺陷this

可是這種方法有2個限制:url

  • cliptPath不支持硬件加速,所以在調用前必須禁用硬件加速,
    setLayerType(View.LAYER_TYPE_SOFTWARE, null)
  • 這種方式剪裁的是Canvas圖形,View的實際形狀是不變的,
    所以只能對src屬性有效,對background屬性是無效的。

1.定義Radius屬性,用來設置圓角半徑code

注意事項:

  • 咱們定義radius爲dimension,這是一個帶單位的值(float不帶單位)
  • radius:值默認或者<0,表示圓形圖;>0表示圓角圖
<declare-styleable name="RoundImageView">
    <attr name="radius" format="dimension" />
</declare-styleable>

2.定義RoundImageView自定義圓形控件

注意事項

  • 設置圓形:path.addCircle
  • 設置圓角:path.addRoundRect
  • canvas.clipPath:不支持硬件加速,因此在使用前須要禁止硬件加速
    setLayerType(View.LAYER_TYPE_SOFTWARE, null)
  • clipPath要在super.onDraw方法前,調用,不然無效(canvas已經被設置給View了)
  • 在onSizeChanged方法中,獲取寬高
public class RoundImageView extends AppCompatImageView {

    private RectF mRect;
    private Path mPath;
    private float mRadius;

    public RoundImageView(Context context) {
        this(context, null);
    }

    public RoundImageView(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public RoundImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getAttributes(context, attrs);
        initView(context);
    }

    /**
     * 獲取屬性
     */
    private void getAttributes(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
        mRadius = ta.getDimension(R.styleable.RoundImageView_radius, -1);
        ta.recycle();
    }

    /**
     * 初始化
     */
    private void initView(Context context) {
        mRect = new RectF();
        mPath = new Path();
        setLayerType(LAYER_TYPE_SOFTWARE, null);        // 禁用硬件加速
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mRadius < 0) {
            clipCircle(w, h);
        } else {
            clipRoundRect(w, h);
        }
    }

    /**
     * 圓角
     */
    private void clipRoundRect(int width, int height) {
        mRect.left = 0;
        mRect.top = 0;
        mRect.right = width;
        mRect.bottom = height;
        mPath.addRoundRect(mRect, mRadius, mRadius, Path.Direction.CW);
    }

    /**
     * 圓形
     */
    private void clipCircle(int width, int height) {
        int radius = Math.min(width, height)/2;
        mPath.addCircle(width/2, height/2, radius, Path.Direction.CW);
    }

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

BitmapShader繪製

思路

經過Canvas.drawCircle本身去繪製一個圓形圖片,並設置給ImageView;

  • 經過drawable資源獲取Bitmap資源
  • 根據Bitmap,建立一個BitmapShader着色器
  • 對BitmapShader作矩陣變化,調整着色器大小至合適的尺寸
  • 將做色器設置給畫筆Paint
  • 調用canvas.drawCircle讓canvas根據畫筆,去繪製一個圓形圖片

缺陷

這種方式有個限制,就是若是要定義一個圓角圖片,必須調用canvas.drawRoundRect進行繪製,可是這個方法要求API>=21

這裏,咱們能夠看到,ImageView底層顯示圖片的原理,就是利用Canvas將咱們的圖片資源給繪製到View控件上

1. 從圖片資源中,獲取Bitmap

Drawable轉Bitmap的2種方式

  • 直接從BitmapDrawable中獲取
  • 利用Canvas去建立一個Bitmap,而後調用drawable.draw(canvas),本身去繪製Bitmap

注意事項:

  • Drawable不能從構造方法中,獲取,這個時候獲取到的是null
  • Drawable分srcbackground
private void initBitmap() {
    Drawable drawable1 = getDrawable();
    Drawable drawable2 = getBackground();
    Drawable drawable = drawable1==null ? drawable2 : drawable1;          // 不能在構造方法中獲取drawable,爲null
    if (drawable instanceof BitmapDrawable) {
        BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
        mBitmap = bitmapDrawable.getBitmap();
    } else {
        int width = drawable.getIntrinsicWidth();       // 圖片的原始寬度
        int height = drawable.getIntrinsicHeight();     // 圖片的原始高度
        mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(mBitmap);
//            drawable.setBounds(0,0,width,height);
        drawable.draw(canvas); 
    }
}

2. 根據Bitmap,建立着色器BitmapShader

BitmapShader着色器

  • TileMode瓷磚類型:當Canvas的寬高大於Bitmap的尺寸時,採起的重複策略

    • TileMode.MIRROR:圖片鏡像鋪開
    • TileMode.REPEAT:圖片重複鋪開
    • TileMode.CLAMP:複用最後一個像素點
  • setLocalMatrix:對着色器中的Bitmap進行矩陣變化
private void initShader(Bitmap bitmap) {
    mShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

    int bitmapWidth = bitmap.getWidth();
    int bitmapHeight = bitmap.getHeight();
    float sx = mWidth * 1.0f / bitmapWidth;
    float sy = mHeight * 1.0f / bitmapHeight;
    float scale = Math.max(sx, sy);

    Matrix matrix = new Matrix();
    matrix.setScale(scale, scale);
    mShader.setLocalMatrix(matrix);
}

3. 將着色器BitmapShader,設置給Paint

mPaint.setShader(mShader);

4. 利用Canvas,本身繪製圓形/圓角圖

注意點:

  • drawRoundRect只適用於Android 21及其以上版本
  • 要刪除 super.onDraw(canvas):不然Canvas又會在ImageView中從新繪製,將咱們以前的操做都覆蓋了
@Override
protected void onDraw(Canvas canvas) {
    initPaint();
    if (mRadius < 0) {
        float radius = Math.min(mWidth, mHeight) / 2;
        canvas.drawCircle(mWidth/2, mHeight/2, radius, mPaint);
    } else {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {    // 21及其以上
            canvas.drawRoundRect(0, 0, mWidth, mHeight, mRadius, mRadius, mPaint);
        } else {
            super.onDraw(canvas);
        }
    }
    // super.onDraw(canvas);
}

完整代碼

public class RoundImageView2 extends AppCompatImageView {

    private int mWidth;
    private int mHeight;
    private float mRadius;
    private Paint mPaint;
    private Bitmap mBitmap;
    private BitmapShader mShader;

    public RoundImageView2(Context context) {
        this(context, null);
    }

    public RoundImageView2(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public RoundImageView2(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getAttributes(context, attrs);
        initView(context);
    }

    private void getAttributes(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
        mRadius = ta.getDimension(R.styleable.RoundImageView_radius, -1);
        ta.recycle();
    }

    private void initView(Context context) {
        mPaint = new Paint();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        initPaint();
        if (mRadius < 0) {
            float radius = Math.min(mWidth, mHeight) / 2;
            canvas.drawCircle(mWidth/2, mHeight/2, radius, mPaint);
        } else {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {    // 21及其以上
                canvas.drawRoundRect(0, 0, mWidth, mHeight, mRadius, mRadius, mPaint);
            } else {
                super.onDraw(canvas);
            }
        }

//        super.onDraw(canvas);
    }

    /**
     * 設置畫筆
     */
    private void initPaint() {
        initBitmap();
        initShader(mBitmap);
        mPaint.setShader(mShader);
    }

    /**
     * 獲取Bitmap
     */
    private void initBitmap() {
        Drawable drawable1 = getDrawable();
        Drawable drawable2 = getBackground();
        Drawable drawable = drawable1==null ? drawable2 : drawable1;          // 不能在構造方法中獲取drawable,爲null
        if (drawable instanceof BitmapDrawable) {
            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            mBitmap = bitmapDrawable.getBitmap();
        } else {
            int width = drawable.getIntrinsicWidth();     
            int height = drawable.getIntrinsicHeight(); 
            mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(mBitmap);
//            drawable.setBounds(0,0,width,height);
            drawable.draw(canvas); 
        }
    }

    /**
     * 獲取BitmapShader
     */
    private void initShader(Bitmap bitmap) {
        mShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

        int bitmapWidth = bitmap.getWidth();
        int bitmapHeight = bitmap.getHeight();
        float sx = mWidth * 1.0f / bitmapWidth;
        float sy = mHeight * 1.0f / bitmapHeight;
        float scale = Math.max(sx, sy);

        Matrix matrix = new Matrix();
        matrix.setScale(scale, scale);
        mShader.setLocalMatrix(matrix);
    }
}

OutlineProvider切割

思路

經過view.setOutlineProvider,給咱們的View控件設置一個圓形輪廓,而後讓View根據輪廓提供者進行切割

這個方法不一樣於前2種,前面2種方法,都是針對Canvas作文章,所以只能適用於圖片的圓形處理;而這個方法是實實在在的對View進行了切割,不只僅侷限於圖片,還能夠針對任何其餘View控件進行剪裁,適用範圍更廣(好比咱們能夠將整個頁面變成一個圓形顯示)

缺陷

可是這個方法有個限制,就是OutlineProvider只能適用於API>=21的版本,沒法兼容低版本

OutlineProvider輪廓提供者

OutlineProvider輪廓提供者,能夠給View提供一個外輪廓,而且讓其根據輪廓進行剪切

  • view.setOutlineProvider:設置輪廓提供者
  • view.setClipToOutline:根據輪廓進行剪切
  • outline.setOval:畫一個圓形輪廓
  • outline.setRect:畫一個矩形輪廓

注意事項:

  • OutlineProvider要求API必須>=21;
  • OutlineProvider必須重寫getOutline方法,其中參數Outline,就是提供給View的輪廓,咱們能夠根據須要自定義形狀

完整代碼

public class RoundImageView3 extends AppCompatImageView {

    private float mRadius;

    public RoundImageView3(Context context) {
        this(context, null);
    }

    public RoundImageView3(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public RoundImageView3(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getAttributes(context, attrs);
        initView();
    }

    private void getAttributes(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
        mRadius = ta.getDimension(R.styleable.RoundImageView_radius, -1);
        ta.recycle();
    }

    private void initView() {
        if (android.os.Build.VERSION.SDK_INT >= 21) {
            ViewOutlineProvider outlineProvider = new ViewOutlineProvider(){
                @Override
                public void getOutline(View view, Outline outline) {
                    int width = view.getWidth();
                    int height = view.getHeight();
                    if (mRadius < 0) {
                        int radius = Math.min(width, height) / 2;
                        Rect rect = new Rect(width/2-radius, height/2-radius, width/2+radius, height/2+radius);
                        outline.setOval(rect);  // API>=21
                    } else {
                        Rect rect = new Rect(0, 0, width, height);
                        outline.setRoundRect(rect, mRadius);
                    }
                }
            };
            setClipToOutline(true);
            setOutlineProvider(outlineProvider);
        }
    }

}

Glide顯示圓形/圓角圖片

思路
經過Glide圖片加載框架實現,咱們只須要給RequestOptions添加一個CircleCrop變換,便可實現圓形圖片效果;若是要實現圓角圖片,則須要本身去定義一個BitmapTransformation

缺陷

沒有缺陷

1. Glide實現圓形圖片

Glide內置了不少針對圖形的Transformation變換,咱們能夠藉助其中的CircleCrop選項很是方便的實現圓形圖片的效果。

  • 建立一個RequestOptions選項
  • 給RequestOptions,添加CircleCrop變換
  • 經過apply,將RequestOptions設置給Glide的RequestBuilder

下面2種方式,均可以實現圓形圖片的效果,只是寫法不同:

public static void loadCircleImage1(Context context, String url, ImageView imageView) {
    Glide.with(context)
            .load(url)
            .apply(RequestOptions.circleCropTransform())
            .into(imageView);
}

public static void loadCircleImage2(Context context, String url, ImageView imageView) {
    RequestOptions options = new RequestOptions()
            .circleCrop();
    Glide.with(context)
            .load(url)
            .apply(options)
            .into(imageView);
}

2. Glide顯示圓角圖片

Glide並無像提供CircleCrop那樣,提供一個圓角圖片的Transformation,所以若是須要顯示圓角圖片,那麼就須要本身去定義一個Transformation。

那麼,要怎麼去定義一個Transformation呢?咱們能夠參考Circrop的作法:

  • 寫一個類繼承BitmapTransformation
  • 重寫transformupdateDiskCacheKeyequalshashCode方法

transform:實現變化的具體細節

  • BitmapPool:能夠用來快速的獲取一個Bitmap的資源池,而且一般要在方法中返回這個獲取到的Bitmap
  • toTransform:須要變化的Bitmap原始資源;須要注意的是,這個原始資源並非最初的Bitmap,在調用這個方法以前Glide已經將原始Bitmap進行了合適的縮放
  • outWidthoutHeight:Bitmap的理想尺寸;須要注意的是,這個尺寸並非Bitmap的尺寸,也不是ImageView的尺寸,Glide給咱們返回的這個尺寸是ImageView的最小寬高值(若是ImageView的寬高都是match_parent,那麼返回的是ImageView的最大寬高值)

CircleCrop的源碼

public class CircleCrop extends BitmapTransformation {
  // The version of this transformation, incremented to correct an error in a previous version.
  // See #455.
  private static final int VERSION = 1;
  private static final String ID = "com.bumptech.glide.load.resource.bitmap.CircleCrop." + VERSION;
  private static final byte[] ID_BYTES = ID.getBytes(CHARSET);

  public CircleCrop() {
    // Intentionally empty.
  }

  /**
   * @deprecated Use {@link #CircleCrop()}.
   */
  @Deprecated
  public CircleCrop(@SuppressWarnings("unused") Context context) {
    this();
  }

  /**
   * @deprecated Use {@link #CircleCrop()}
   */
  @Deprecated
  public CircleCrop(@SuppressWarnings("unused") BitmapPool bitmapPool) {
    this();
  }

  // Bitmap doesn't implement equals, so == and .equals are equivalent here.
  @SuppressWarnings("PMD.CompareObjectsWithEquals")
  @Override
  protected Bitmap transform(
      @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
    return TransformationUtils.circleCrop(pool, toTransform, outWidth, outHeight);
  }

  @Override
  public boolean equals(Object o) {
    return o instanceof CircleCrop;
  }

  @Override
  public int hashCode() {
    return ID.hashCode();
  }

  @Override
  public void updateDiskCacheKey(MessageDigest messageDigest) {
    messageDigest.update(ID_BYTES);
  }
}

自定義的一個圓角BitmapTransformation

這個實現細節,與前面的「利用BitmapShader繪製一個圓角圖片」基本是同樣的。

public class GlideRoundRect extends BitmapTransformation {

    private float mRadius;
    private static final int VERSION = 1;
    private static final String ID = BuildConfig.APPLICATION_ID + ".GlideRoundRect." + VERSION;
    private static final byte[] ID_BYTES = ID.getBytes(CHARSET);
    @Override
    public void updateDiskCacheKey(MessageDigest messageDigest) {
        messageDigest.update(ID_BYTES);
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof GlideRoundRect;
    }

    @Override
    public int hashCode() {
        return ID.hashCode();
    }

    public GlideRoundRect(float radius) {
        super();
        mRadius = radius;
    }

    @Override
    protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
        return roundRectCrop(pool, toTransform);
    }

    private Bitmap roundRectCrop(BitmapPool pool, Bitmap source) {
        if (source == null)
            return null;
        // 1. 根據source,建立一個BitmapShader
        BitmapShader bitmapShader = new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        Paint paint = new Paint();
        paint.setShader(bitmapShader);
        // 2. 獲取一個新的Bitmap
        int sourceWidth = source.getWidth();
        int sourceHeight = source.getHeight();
        Bitmap bitmap = pool.get(sourceWidth, sourceHeight, Bitmap.Config.ARGB_8888);
        // 3. 給新的Bitmap附上圖形
        Canvas canvas = new Canvas(bitmap);
        RectF rect = new RectF(0, 0, sourceWidth, sourceHeight);
        canvas.drawRoundRect(rect, mRadius, mRadius, paint);
        // 4. 返回Bitmap
        return bitmap;
    }

}
相關文章
相關標籤/搜索