org.getlantern.firetweet.view.ShapedImageView.java Source code

Java tutorial

Introduction

Here is the source code for org.getlantern.firetweet.view.ShapedImageView.java

Source

/*
 * Firetweet - Twitter client for Android
 *
 *  Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.getlantern.firetweet.view;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Matrix.ScaleToFit;
import android.graphics.Paint;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Build;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;

import org.getlantern.firetweet.R;
import org.getlantern.firetweet.util.accessor.ViewAccessor;
import org.getlantern.firetweet.util.accessor.ViewAccessor.OutlineCompat;
import org.getlantern.firetweet.util.accessor.ViewAccessor.ViewOutlineProviderCompat;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * An ImageView class with a circle mask so that all images are drawn in a
 * circle instead of a square.
 */
public class ShapedImageView extends ImageView {

    @ShapeStyle
    public static final int SHAPE_CIRCLE = 0x1;
    @ShapeStyle
    public static final int SHAPE_RECTANGLE = 0x2;
    private static final int SHADOW_START_COLOR = 0x37000000;
    private static final boolean USE_OUTLINE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
    private static final boolean OUTLINE_DRAW = false;
    private final Matrix mMatrix;
    private final RectF mSource;
    private final RectF mDestination;
    private final RectF mTempDestination;
    private final Paint mBitmapPaint;
    private final Paint mSolidColorPaint;
    private final Paint mBorderPaint;
    private final Paint mBackgroundPaint;
    private boolean mBorderEnabled;
    private Bitmap mShadowBitmap;
    private float mShadowRadius;
    private int mStyle;
    private float mCornerRadius, mCornerRadiusRatio;
    private RectF mTransitionSource, mTransitionDestination;
    private int mStrokeWidth, mBorderAlpha;
    private int[] mBorderColors;

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

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

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

        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ShapedImageView, defStyle, 0);

        mMatrix = new Matrix();
        mSource = new RectF();
        mDestination = new RectF();
        mTempDestination = new RectF();

        mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBitmapPaint.setFilterBitmap(true);
        mBitmapPaint.setDither(true);
        mSolidColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBorderPaint.setStyle(Paint.Style.STROKE);

        mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        if (a.hasValue(R.styleable.ShapedImageView_sivBorder)) {
            setBorderEnabled(a.getBoolean(R.styleable.ShapedImageView_sivBorder, false));
        } else if (a.hasValue(R.styleable.ShapedImageView_sivBorderColor)
                || a.hasValue(R.styleable.ShapedImageView_sivBorderWidth)) {
            setBorderEnabled(true);
        }
        setBorderColor(a.getColor(R.styleable.ShapedImageView_sivBorderColor, Color.TRANSPARENT));
        setBorderWidth(a.getDimensionPixelSize(R.styleable.ShapedImageView_sivBorderWidth, 0));
        @ShapeStyle
        final int shapeStyle = a.getInt(R.styleable.ShapedImageView_sivShape, SHAPE_RECTANGLE);
        setStyle(shapeStyle);
        setCornerRadius(a.getDimension(R.styleable.ShapedImageView_sivCornerRadius, 0));
        setCornerRadiusRatio(a.getFraction(R.styleable.ShapedImageView_sivCornerRadiusRatio, 1, 1, -1));

        if (USE_OUTLINE) {
            if (a.hasValue(R.styleable.ShapedImageView_sivElevation)) {
                ViewCompat.setElevation(this, a.getDimensionPixelSize(R.styleable.ShapedImageView_sivElevation, 0));
            }
        } else {
            mShadowRadius = a.getDimensionPixelSize(R.styleable.ShapedImageView_sivElevation, 0);
        }
        setBackgroundColor(a.getColor(R.styleable.ShapedImageView_sivBackgroundColor, 0));
        a.recycle();

        if (USE_OUTLINE) {
            initOutlineProvider();
        }
    }

    /**
     * Given the source bitmap and a canvas, draws the bitmap through a circular
     * mask. Only draws a circle with diameter equal to the destination width.
     *
     * @param bitmap The source bitmap to draw.
     * @param canvas The canvas to draw it on.
     * @param source The source bound of the bitmap.
     * @param dest   The destination bound on the canvas.
     */
    public void drawBitmapWithCircleOnCanvas(Bitmap bitmap, Canvas canvas, RectF source, @NonNull RectF dest) {
        if (bitmap == null) {
            if (getStyle() == SHAPE_CIRCLE) {
                canvas.drawCircle(dest.centerX(), dest.centerY(), Math.min(dest.width(), dest.height()) / 2f,
                        mSolidColorPaint);
            } else {
                final float cornerRadius = getCalculatedCornerRadius();
                canvas.drawRoundRect(dest, cornerRadius, cornerRadius, mSolidColorPaint);
            }
            return;
        }
        // Draw bitmap through shader first.
        final BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        mMatrix.reset();

        switch (getScaleType()) {
        case CENTER_CROP: {
            final float srcRatio = source.width() / source.height();
            final float dstRatio = dest.width() / dest.height();
            if (srcRatio > dstRatio) {
                // Source is wider than destination, fit height
                mTempDestination.top = dest.top;
                mTempDestination.bottom = dest.bottom;
                final float dstWidth = dest.height() * srcRatio;
                mTempDestination.left = dest.centerX() - dstWidth / 2;
                mTempDestination.right = dest.centerX() + dstWidth / 2;
            } else if (srcRatio < dstRatio) {
                mTempDestination.left = dest.left;
                mTempDestination.right = dest.right;
                final float dstHeight = dest.width() / srcRatio;
                mTempDestination.top = dest.centerY() - dstHeight / 2;
                mTempDestination.bottom = dest.centerY() + dstHeight / 2;
            } else {
                mTempDestination.set(dest);
            }
            break;
        }
        default: {
            mTempDestination.set(dest);
            break;
        }
        }

        // Fit bitmap to bounds.
        mMatrix.setRectToRect(source, mTempDestination, ScaleToFit.CENTER);

        shader.setLocalMatrix(mMatrix);
        mBitmapPaint.setShader(shader);

        if (mBorderEnabled) {
            final float inset = mBorderPaint.getStrokeWidth() / 2;
            if (getStyle() == SHAPE_CIRCLE) {
                final float circleRadius = Math.min(dest.width(), dest.height()) / 2f - inset / 2;
                canvas.drawCircle(dest.centerX(), dest.centerY(), circleRadius, mBitmapPaint);
            } else {
                final float cornerRadius = getCalculatedCornerRadius();
                dest.inset(inset, inset);
                canvas.drawRoundRect(dest, cornerRadius, cornerRadius, mBitmapPaint);
                dest.inset(-inset, -inset);
            }
        } else {
            if (getStyle() == SHAPE_CIRCLE) {
                final float circleRadius = Math.min(dest.width(), dest.height()) / 2f;
                canvas.drawCircle(dest.centerX(), dest.centerY(), circleRadius, mBitmapPaint);
            } else {
                final float cornerRadius = getCalculatedCornerRadius();
                canvas.drawRoundRect(dest, cornerRadius, cornerRadius, mBitmapPaint);
            }
        }

    }

    public int[] getBorderColors() {
        return mBorderColors;
    }

    @ShapeStyle
    public int getStyle() {
        return mStyle;
    }

    public void setStyle(@ShapeStyle final int style) {
        mStyle = style;
    }

    public void setBorderColor(int color) {
        setBorderColorsInternal(Color.alpha(color), color);
    }

    public void setBorderColors(int... colors) {
        setBorderColorsInternal(0xff, colors);
    }

    public void setBorderEnabled(boolean enabled) {
        mBorderEnabled = enabled;
        invalidate();
    }

    public void setBorderWidth(int width) {
        mBorderPaint.setStrokeWidth(width);
        mStrokeWidth = width;
        invalidate();
    }

    public void setCornerRadius(float radius) {
        mCornerRadius = radius;
    }

    public void setCornerRadiusRatio(float ratio) {
        mCornerRadiusRatio = ratio;
    }

    public void setTransitionDestination(RectF dstBounds) {
        mTransitionDestination = dstBounds;
    }

    public void setTransitionSource(RectF srcBounds) {
        mTransitionSource = srcBounds;
    }

    @Override
    protected void onDraw(@NonNull Canvas canvas) {
        mDestination.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
                getHeight() - getPaddingBottom());

        if (getStyle() == SHAPE_CIRCLE) {
            canvas.drawOval(mDestination, mBackgroundPaint);
        } else {
            final float radius = getCalculatedCornerRadius();
            canvas.drawRoundRect(mDestination, radius, radius, mBackgroundPaint);
        }

        if (OUTLINE_DRAW) {
            super.onDraw(canvas);
        } else {
            final int contentLeft = getPaddingLeft(), contentTop = getPaddingTop(),
                    contentRight = getWidth() - getPaddingRight(), contentBottom = getHeight() - getPaddingBottom();
            final int contentWidth = contentRight - contentLeft, contentHeight = contentBottom - contentTop;
            final int size = Math.min(contentWidth, contentHeight);
            if (mShadowBitmap != null) {
                canvas.drawBitmap(mShadowBitmap, contentLeft + (contentWidth - size) / 2 - mShadowRadius,
                        contentTop + (contentHeight - size) / 2 - mShadowRadius, null);
            }
            Drawable drawable = getDrawable();
            BitmapDrawable bitmapDrawable = null;
            // support state list drawable by getting the current state
            if (drawable instanceof StateListDrawable) {
                if (drawable.getCurrent() != null) {
                    bitmapDrawable = (BitmapDrawable) drawable.getCurrent();
                }
            } else if (drawable instanceof BitmapDrawable) {
                bitmapDrawable = (BitmapDrawable) drawable;
            } else if (drawable instanceof ColorDrawable) {
                mSolidColorPaint.setColor(((ColorDrawable) drawable).getColor());
            } else {
                mSolidColorPaint.setColor(0);
            }

            Bitmap bitmap = null;
            if (bitmapDrawable != null) {
                bitmap = bitmapDrawable.getBitmap();
            }
            if (bitmap != null) {
                mSource.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
            }
            drawBitmapWithCircleOnCanvas(bitmap, canvas, mSource, mDestination);
        }

        // Then draw the border.
        if (mBorderEnabled) {
            drawBorder(canvas, mDestination);
        }
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        if (OUTLINE_DRAW) {
            super.setColorFilter(cf);
            return;
        }
        mBitmapPaint.setColorFilter(cf);
    }

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

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

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        updateBounds();
    }

    @Override
    public void setBackgroundColor(int color) {
        mBackgroundPaint.setColor(0xFF000000 | color);
        mBackgroundPaint.setAlpha(Color.alpha(color));
        invalidate();
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void setBackground(Drawable background) {
    }

    @Deprecated
    @Override
    public void setBackgroundDrawable(Drawable background) {
        // No-op
    }

    @Override
    public void setPadding(int left, int top, int right, int bottom) {
        super.setPadding(left, top, right, bottom);
        updateBounds();
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    @Override
    public void setPaddingRelative(int start, int top, int end, int bottom) {
        super.setPaddingRelative(start, top, end, bottom);
        updateBounds();
    }

    private void drawBorder(@NonNull final Canvas canvas, @NonNull final RectF dest) {
        if (mBorderColors == null)
            return;
        final RectF transitionSrc = mTransitionSource, transitionDst = mTransitionDestination;
        final float strokeWidth;
        if (transitionSrc != null && transitionDst != null) {
            final float progress = 1
                    - (dest.width() - transitionDst.width()) / (transitionSrc.width() - transitionDst.width());
            strokeWidth = mStrokeWidth * progress;
            mBorderPaint.setAlpha(Math.round(mBorderAlpha * progress));
            ViewCompat.setTranslationZ(this, -ViewCompat.getElevation(this) * (1 - progress));
        } else {
            strokeWidth = mStrokeWidth;
            mBorderPaint.setAlpha(mBorderAlpha);
            ViewCompat.setTranslationZ(this, 0);
        }
        mBorderPaint.setStrokeWidth(strokeWidth);
        if (getStyle() == SHAPE_CIRCLE) {
            final float circleRadius = Math.min(dest.width(), dest.height()) / 2f - strokeWidth / 2;
            canvas.drawCircle(dest.centerX(), dest.centerY(), circleRadius, mBorderPaint);
        } else {
            final float radius = getCalculatedCornerRadius();
            final float inset = mStrokeWidth / 2;
            dest.inset(inset, inset);
            canvas.drawRoundRect(dest, radius, radius, mBorderPaint);
            dest.inset(-inset, -inset);
        }
    }

    private float getCalculatedCornerRadius() {
        if (mCornerRadiusRatio > 0) {
            return Math.min(getWidth(), getHeight()) * mCornerRadiusRatio;
        } else if (mCornerRadius > 0) {
            return mCornerRadius;
        }
        return 0;
    }

    private void initOutlineProvider() {
        ViewAccessor.setClipToOutline(this, true);
        ViewAccessor.setOutlineProvider(this, new CircularOutlineProvider());
    }

    private void setBorderColorsInternal(int alpha, int... colors) {
        mBorderAlpha = alpha;
        mBorderColors = colors;
        updateBorderShader();
        invalidate();
    }

    private void updateBorderShader() {
        final int[] colors = mBorderColors;
        if (colors == null || colors.length == 0) {
            mBorderAlpha = 0;
            return;
        }
        mDestination.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
                getHeight() - getPaddingBottom());
        final float cx = mDestination.centerX(), cy = mDestination.centerY();
        final int[] sweepColors = new int[colors.length * 2];
        final float[] positions = new float[colors.length * 2];
        for (int i = 0, j = colors.length; i < j; i++) {
            sweepColors[i * 2] = sweepColors[i * 2 + 1] = colors[i];
            positions[i * 2] = i == 0 ? 0 : i / (float) j;
            positions[i * 2 + 1] = i == j - 1 ? 1 : (i + 1) / (float) j;
        }
        final SweepGradient shader = new SweepGradient(cx, cy, sweepColors, positions);
        final Matrix matrix = new Matrix();
        matrix.setRotate(90, cx, cy);
        shader.setLocalMatrix(matrix);
        mBorderPaint.setShader(shader);
    }

    private void updateBounds() {
        updateBorderShader();
        updateShadowBitmap();
    }

    private void updateShadowBitmap() {
        if (USE_OUTLINE)
            return;
        final int width = getWidth(), height = getHeight();
        if (width <= 0 || height <= 0)
            return;
        final int contentLeft = getPaddingLeft(), contentTop = getPaddingTop(),
                contentRight = width - getPaddingRight(), contentBottom = height - getPaddingBottom();
        final int contentWidth = contentRight - contentLeft, contentHeight = contentBottom - contentTop;
        final float radius = mShadowRadius, dy = radius * 1.5f / 2;
        final int size = Math.round(Math.min(contentWidth, contentHeight) + radius * 2);
        mShadowBitmap = Bitmap.createBitmap(size, Math.round(size + dy), Config.ARGB_8888);
        Canvas canvas = new Canvas(mShadowBitmap);
        final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(0xFF000000 | mBackgroundPaint.getColor());
        paint.setShadowLayer(radius, 0, radius * 1.5f / 2, SHADOW_START_COLOR);
        final RectF rect = new RectF(radius, radius, size - radius, size - radius);
        if (getStyle() == SHAPE_CIRCLE) {
            canvas.drawOval(rect, paint);
            paint.setShadowLayer(0, 0, 0, 0);
            paint.setXfermode(new PorterDuffXfermode(Mode.CLEAR));
            canvas.drawOval(rect, paint);
        } else {
            final float cr = getCalculatedCornerRadius();
            canvas.drawRoundRect(rect, cr, cr, paint);
            paint.setShadowLayer(0, 0, 0, 0);
            paint.setXfermode(new PorterDuffXfermode(Mode.CLEAR));
            canvas.drawRoundRect(rect, cr, cr, paint);
        }
        invalidate();
    }

    @IntDef({ SHAPE_CIRCLE, SHAPE_RECTANGLE })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ShapeStyle {
    }

    private static class CircularOutlineProvider extends ViewOutlineProviderCompat {
        @Override
        public void getOutline(View view, OutlineCompat outline) {
            final int contentLeft = view.getPaddingLeft(), contentTop = view.getPaddingTop(),
                    contentRight = view.getWidth() - view.getPaddingRight(),
                    contentBottom = view.getHeight() - view.getPaddingBottom();
            final ShapedImageView imageView = (ShapedImageView) view;
            if (imageView.getStyle() == SHAPE_CIRCLE) {
                final int contentWidth = contentRight - contentLeft, contentHeight = contentBottom - contentTop;
                final int size = Math.min(contentWidth, contentHeight);
                outline.setOval(contentLeft + (contentWidth - size) / 2, contentTop + (contentHeight - size) / 2,
                        contentRight - (contentWidth - size) / 2, contentBottom - (contentHeight - size) / 2);
            } else {
                final float radius = imageView.getCalculatedCornerRadius();
                outline.setRoundRect(contentLeft, contentTop, contentRight, contentBottom, radius);
            }
        }
    }
}