FitWidthBitmapDrawable.java

/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.leanback.graphics;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.IntProperty;
import android.util.Property;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;

/**
 * Subclass of {@link Drawable} that can be used to draw a bitmap into a region. Bitmap
 * will be scaled to fit the full width of the region and will be aligned to the top left corner.
 * Any region outside the bounds will be clipped during {@link #draw(Canvas)} call. Top
 * position of the bitmap can be controlled by {@link #setVerticalOffset(int)} call or
 * {@link #PROPERTY_VERTICAL_OFFSET}.
 */
public class FitWidthBitmapDrawable extends Drawable {

    static class BitmapState extends Drawable.ConstantState {
        Paint mPaint;
        Bitmap mBitmap;
        Rect mSource;
        final Rect mDefaultSource = new Rect();
        int mOffset;

        BitmapState() {
            mPaint = new Paint();
        }

        BitmapState(BitmapState other) {
            mBitmap = other.mBitmap;
            mPaint = new Paint(other.mPaint);
            mSource = other.mSource != null ? new Rect(other.mSource) : null;
            mDefaultSource.set(other.mDefaultSource);
            mOffset = other.mOffset;
        }

        @NonNull
        @Override
        public Drawable newDrawable() {
            return new FitWidthBitmapDrawable(this);
        }

        @Override
        public int getChangingConfigurations() {
            return 0;
        }
    }

    final Rect mDest = new Rect();
    BitmapState mBitmapState;
    boolean mMutated = false;

    public FitWidthBitmapDrawable() {
        mBitmapState = new BitmapState();
    }

    FitWidthBitmapDrawable(BitmapState state) {
        mBitmapState = state;
    }

    @Override
    public ConstantState getConstantState() {
        return mBitmapState;
    }

    @Override
    public Drawable mutate() {
        if (!mMutated && super.mutate() == this) {
            mBitmapState = new BitmapState(mBitmapState);
            mMutated = true;
        }
        return this;
    }

    /**
     * Sets the bitmap.
     */
    public void setBitmap(Bitmap bitmap) {
        mBitmapState.mBitmap = bitmap;
        if (bitmap != null) {
            mBitmapState.mDefaultSource.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
        } else {
            mBitmapState.mDefaultSource.set(0, 0, 0, 0);
        }
        mBitmapState.mSource = null;
    }

    /**
     * Returns the bitmap.
     */
    public Bitmap getBitmap() {
        return mBitmapState.mBitmap;
    }

    /**
     * Sets the {@link Rect} used for extracting the bitmap.
     */
    public void setSource(Rect source) {
        mBitmapState.mSource = source;
    }

    /**
     * Returns the {@link Rect} used for extracting the bitmap.
     */
    public Rect getSource() {
        return mBitmapState.mSource;
    }

    /**
     * Sets the vertical offset which will be used for drawing the bitmap. The bitmap drawing
     * will start the provided vertical offset.
     * @see #PROPERTY_VERTICAL_OFFSET
     */
    public void setVerticalOffset(int offset) {
        mBitmapState.mOffset = offset;
        invalidateSelf();
    }

    /**
     * Returns the current vertical offset.
     * @see #PROPERTY_VERTICAL_OFFSET
     */
    public int getVerticalOffset() {
        return mBitmapState.mOffset;
    }

    @Override
    public void draw(Canvas canvas) {
        if (mBitmapState.mBitmap != null) {
            Rect bounds = getBounds();
            mDest.left = 0;
            mDest.top = mBitmapState.mOffset;
            mDest.right = bounds.width();

            Rect source = validateSource();
            float scale = (float) bounds.width() / source.width();
            mDest.bottom = mDest.top + (int) (source.height() * scale);
            int i = canvas.save();
            canvas.clipRect(bounds);
            canvas.drawBitmap(mBitmapState.mBitmap, source, mDest, mBitmapState.mPaint);
            canvas.restoreToCount(i);
        }
    }

    @Override
    public void setAlpha(int alpha) {
        final int oldAlpha = mBitmapState.mPaint.getAlpha();
        if (alpha != oldAlpha) {
            mBitmapState.mPaint.setAlpha(alpha);
            invalidateSelf();
        }
    }

    /**
     * @return Alpha value between 0(inclusive) and 255(inclusive)
     */
    @Override
    public int getAlpha() {
        return mBitmapState.mPaint.getAlpha();
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        mBitmapState.mPaint.setColorFilter(colorFilter);
        invalidateSelf();
    }

    @Override
    public int getOpacity() {
        final Bitmap bitmap = mBitmapState.mBitmap;
        return (bitmap == null || bitmap.hasAlpha() || mBitmapState.mPaint.getAlpha() < 255)
                ? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
    }

    private Rect validateSource() {
        if (mBitmapState.mSource == null) {
            return mBitmapState.mDefaultSource;
        } else {
            return mBitmapState.mSource;
        }
    }

    /**
     * Property for {@link #setVerticalOffset(int)} and {@link #getVerticalOffset()}.
     */
    public static final Property<FitWidthBitmapDrawable, Integer> PROPERTY_VERTICAL_OFFSET;

    static {
        if (Build.VERSION.SDK_INT >= 24) {
            // use IntProperty
            PROPERTY_VERTICAL_OFFSET = getVerticalOffsetIntProperty();
        } else {
            // use Property
            PROPERTY_VERTICAL_OFFSET = new Property<FitWidthBitmapDrawable, Integer>(Integer.class,
                    "verticalOffset") {
                @Override
                public void set(FitWidthBitmapDrawable object, Integer value) {
                    object.setVerticalOffset(value);
                }

                @Override
                public Integer get(FitWidthBitmapDrawable object) {
                    return object.getVerticalOffset();
                }
            };
        }
    }

    @RequiresApi(24)
    static IntProperty<FitWidthBitmapDrawable> getVerticalOffsetIntProperty() {
        return new IntProperty<FitWidthBitmapDrawable>("verticalOffset") {
            @Override
            public void setValue(FitWidthBitmapDrawable fitWidthBitmapDrawable, int value) {
                fitWidthBitmapDrawable.setVerticalOffset(value);
            }

            @Override
            public Integer get(FitWidthBitmapDrawable fitWidthBitmapDrawable) {
                return fitWidthBitmapDrawable.getVerticalOffset();
            }
        };
    }
}