chromium/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/RoundedCornerImageView.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.browser_ui.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.graphics.drawable.shapes.Shape;
import android.util.AttributeSet;
import android.widget.ImageView;

import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.core.view.ViewCompat;

/**
 * A custom {@link ImageView} that is able to render bitmaps and colors with rounded off corners.
 * The corner radii should be set through attributes. E.g.
 *
 *   <org.chromium.components.browser_ui.widget.RoundedCornerImageView
 *      app:cornerRadiusTopStart="8dp"
 *      app:cornerRadiusTopEnd="8dp"
 *      app:cornerRadiusBottomStart="8dp"
 *      app:cornerRadiusBottomEnd="8dp"
 *      app:roundedfillColor="@android:color/white"/>
 *
 * Note : This does not properly handle padding. Padding will not be taken into account when rounded
 * corners are used.
 */
public class RoundedCornerImageView extends AppCompatImageView {
    private final RectF mTmpRect = new RectF();
    private final Matrix mTmpMatrix = new Matrix();

    private final Paint mRoundedBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private Paint mRoundedContentPaint;
    private boolean mRoundCorners;
    // True, if constructor had a chance to run.
    // This is needed, because ImageView's constructor may trigger updates on our end
    // if certain attributes (eg. Drawable) are supplied via layout attributes.
    private final boolean mIsInitialized;

    private Shape mRoundedRectangle;
    private @ColorInt int mFillColor = Color.TRANSPARENT;

    public RoundedCornerImageView(Context context) {
        this(context, null, 0);
    }

    public RoundedCornerImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RoundedCornerImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        // Set attribute indicating that all required objects are created.
        mIsInitialized = true;

        int radiusTopStart = 0;
        int radiusTopEnd = 0;
        int radiusBottomStart = 0;
        int radiusBottomEnd = 0;
        int color = Color.TRANSPARENT;

        if (attrs != null) {
            TypedArray a =
                    getContext()
                            .obtainStyledAttributes(
                                    attrs, R.styleable.RoundedCornerImageView, 0, 0);
            radiusTopStart =
                    a.getDimensionPixelSize(
                            R.styleable.RoundedCornerImageView_cornerRadiusTopStart, 0);
            radiusTopEnd =
                    a.getDimensionPixelSize(
                            R.styleable.RoundedCornerImageView_cornerRadiusTopEnd, 0);
            radiusBottomStart =
                    a.getDimensionPixelSize(
                            R.styleable.RoundedCornerImageView_cornerRadiusBottomStart, 0);
            radiusBottomEnd =
                    a.getDimensionPixelSize(
                            R.styleable.RoundedCornerImageView_cornerRadiusBottomEnd, 0);

            color =
                    a.getColor(
                            R.styleable.RoundedCornerImageView_roundedfillColor, Color.TRANSPARENT);
            a.recycle();
        }

        setRoundedCorners(radiusTopStart, radiusTopEnd, radiusBottomStart, radiusBottomEnd);
        setRoundedFillColor(color);
        refreshState();
    }

    /**
     * Sets the rounded corner fill color to {@code color}.  This can be used to make sure the
     * rounded shape shows even if the actual content isn't full-bleed (e.g. icon with transparency
     * or too small to reach the edges).
     * @param color The color to use.  Setting to {@link Color#TRANSPARENT} will remove the color.
     */
    public void setRoundedFillColor(@ColorInt int color) {
        mFillColor = color;
        mRoundedBackgroundPaint.setColor(color);
        invalidate();
    }

    // ImageView implementation.
    @Override
    public void setImageDrawable(@Nullable Drawable drawable) {
        super.setImageDrawable(drawable);
        refreshState();
    }

    @Override
    public void setImageResource(int resId) {
        super.setImageResource(resId);
        refreshState();
    }

    @Override
    public void setImageBitmap(Bitmap bm) {
        super.setImageBitmap(bm);
        refreshState();
    }

    public void setRoundedCorners(
            int cornerRadiusTopStart,
            int cornerRadiusTopEnd,
            int cornerRadiusBottomStart,
            int cornerRadiusBottomEnd) {
        mRoundCorners =
                (cornerRadiusTopStart != 0
                        || cornerRadiusTopEnd != 0
                        || cornerRadiusBottomStart != 0
                        || cornerRadiusBottomEnd != 0);
        if (!mRoundCorners) return;

        float[] radii;
        if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) {
            radii =
                    new float[] {
                        cornerRadiusTopStart,
                        cornerRadiusTopStart,
                        cornerRadiusTopEnd,
                        cornerRadiusTopEnd,
                        cornerRadiusBottomEnd,
                        cornerRadiusBottomEnd,
                        cornerRadiusBottomStart,
                        cornerRadiusBottomStart
                    };
        } else {
            radii =
                    new float[] {
                        cornerRadiusTopEnd,
                        cornerRadiusTopEnd,
                        cornerRadiusTopStart,
                        cornerRadiusTopStart,
                        cornerRadiusBottomStart,
                        cornerRadiusBottomStart,
                        cornerRadiusBottomEnd,
                        cornerRadiusBottomEnd
                    };
        }

        mRoundedRectangle = new RoundRectShape(radii, null, null);
    }

    private void refreshState() {
        Drawable drawable = getDrawable();

        // Do not update state if we were invoked from the ImageView's constructor
        // (before we had the chance to initialize our own private data).
        if (!mIsInitialized) {
            return;
        }

        if (drawable instanceof ColorDrawable) {
            mRoundedBackgroundPaint.setColor(((ColorDrawable) getDrawable()).getColor());
            mRoundedContentPaint = null;
        } else if (drawable instanceof BitmapDrawable
                && ((BitmapDrawable) drawable).getBitmap() != null) {
            mRoundedBackgroundPaint.setColor(mFillColor);
            mRoundedContentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

            Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();

            mRoundedContentPaint.setShader(
                    new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
        } else {
            mRoundedBackgroundPaint.setColor(mFillColor);
            mRoundedContentPaint = null;
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (!mRoundCorners) {
            super.onDraw(canvas);
            return;
        }

        final int width = getWidth() - getPaddingLeft() - getPaddingRight();
        final int height = getHeight() - getPaddingTop() - getPaddingBottom();
        if (width <= 0 || height <= 0) return;

        mRoundedRectangle.resize(width, height);

        final int saveCount = canvas.save();
        try {
            canvas.translate(getPaddingLeft(), getPaddingTop());

            if (mRoundedBackgroundPaint.getColor() != Color.TRANSPARENT) {
                mRoundedRectangle.draw(canvas, mRoundedBackgroundPaint);
                // Note: RoundedBackgroundPaint is also used as ColorDrawable.
                if (getDrawable() instanceof ColorDrawable) {
                    return;
                }
            }

            if (mRoundedContentPaint == null) {
                canvas.restoreToCount(saveCount);
                super.onDraw(canvas);
                return;
            }

            Shader shader = mRoundedContentPaint.getShader();
            if (shader != null) {
                Drawable drawable = getDrawable();
                Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
                mTmpMatrix.set(getImageMatrix());
                mTmpMatrix.preScale(
                        (float) drawable.getIntrinsicWidth() / bitmap.getWidth(),
                        (float) drawable.getIntrinsicHeight() / bitmap.getHeight());

                shader.setLocalMatrix(mTmpMatrix);

                mTmpRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
                mTmpMatrix.mapRect(mTmpRect);
                canvas.clipRect(mTmpRect);
            }

            mRoundedRectangle.draw(canvas, mRoundedContentPaint);
        } finally {
            canvas.restoreToCount(saveCount);
        }
    }
}