/* * Copyright (C) 2014 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 com.android.bitmap.drawable; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; import android.view.View; import com.android.bitmap.BitmapCache; /** * A custom ExtendedBitmapDrawable that styles the corners in configurable ways. * * All four corners can be configured as {@link #CORNER_STYLE_SHARP}, * {@link #CORNER_STYLE_ROUND}, or {@link #CORNER_STYLE_FLAP}. * This is accomplished applying a non-rectangular clip applied to the canvas. * * A border is draw that conforms to the styled corners. * * {@link #CORNER_STYLE_FLAP} corners have a colored flap drawn within the bounds. */ public class StyledCornersBitmapDrawable extends ExtendedBitmapDrawable { private static final String TAG = StyledCornersBitmapDrawable.class.getSimpleName(); public static final int CORNER_STYLE_SHARP = 0; public static final int CORNER_STYLE_ROUND = 1; public static final int CORNER_STYLE_FLAP = 2; private static final int START_RIGHT = 0; private static final int START_BOTTOM = 90; private static final int START_LEFT = 180; private static final int START_TOP = 270; private static final int QUARTER_CIRCLE = 90; private static final RectF sRectF = new RectF(); private final Paint mFlapPaint = new Paint(); private final Paint mBorderPaint = new Paint(); private final Paint mCompatibilityModeBackgroundPaint = new Paint(); private final Path mClipPath = new Path(); private final Path mCompatibilityModePath = new Path(); private final float mCornerRoundRadius; private final float mCornerFlapSide; private int mTopLeftCornerStyle = CORNER_STYLE_SHARP; private int mTopRightCornerStyle = CORNER_STYLE_SHARP; private int mBottomRightCornerStyle = CORNER_STYLE_SHARP; private int mBottomLeftCornerStyle = CORNER_STYLE_SHARP; private int mTopStartCornerStyle = CORNER_STYLE_SHARP; private int mTopEndCornerStyle = CORNER_STYLE_SHARP; private int mBottomEndCornerStyle = CORNER_STYLE_SHARP; private int mBottomStartCornerStyle = CORNER_STYLE_SHARP; private int mScrimColor; private float mBorderWidth; private boolean mIsCompatibilityMode; private boolean mEatInvalidates; /** * Create a new StyledCornersBitmapDrawable. */ public StyledCornersBitmapDrawable(Resources res, BitmapCache cache, boolean limitDensity, ExtendedOptions opts, float cornerRoundRadius, float cornerFlapSide) { super(res, cache, limitDensity, opts); mCornerRoundRadius = cornerRoundRadius; mCornerFlapSide = cornerFlapSide; mFlapPaint.setColor(Color.TRANSPARENT); mFlapPaint.setStyle(Style.FILL); mFlapPaint.setAntiAlias(true); mBorderPaint.setColor(Color.TRANSPARENT); mBorderPaint.setStyle(Style.STROKE); mBorderPaint.setStrokeWidth(mBorderWidth); mBorderPaint.setAntiAlias(true); mCompatibilityModeBackgroundPaint.setColor(Color.TRANSPARENT); mCompatibilityModeBackgroundPaint.setStyle(Style.FILL); mCompatibilityModeBackgroundPaint.setAntiAlias(true); mScrimColor = Color.TRANSPARENT; } /** * Set the border stroke width of this drawable. */ public void setBorderWidth(final float borderWidth) { final boolean changed = mBorderPaint.getStrokeWidth() != borderWidth; mBorderPaint.setStrokeWidth(borderWidth); mBorderWidth = borderWidth; if (changed) { invalidateSelf(); } } /** * Set the border stroke color of this drawable. Set to {@link Color#TRANSPARENT} to disable. */ public void setBorderColor(final int color) { final boolean changed = mBorderPaint.getColor() != color; mBorderPaint.setColor(color); if (changed) { invalidateSelf(); } } /** Set the corner styles for all four corners specified in RTL friendly ways */ public void setCornerStylesRelative(int topStart, int topEnd, int bottomEnd, int bottomStart) { mTopStartCornerStyle = topStart; mTopEndCornerStyle = topEnd; mBottomEndCornerStyle = bottomEnd; mBottomStartCornerStyle = bottomStart; resolveCornerStyles(); } @Override public void onLayoutDirectionChangeLocal(int layoutDirection) { resolveCornerStyles(); } /** * Get the flap color for all corners that have style {@link #CORNER_STYLE_SHARP}. */ public int getFlapColor() { return mFlapPaint.getColor(); } /** * Set the flap color for all corners that have style {@link #CORNER_STYLE_SHARP}. * * Use {@link android.graphics.Color#TRANSPARENT} to disable flap colors. */ public void setFlapColor(int flapColor) { boolean changed = mFlapPaint.getColor() != flapColor; mFlapPaint.setColor(flapColor); if (changed) { invalidateSelf(); } } /** * Get the color of the scrim that is drawn over the contents, but under the flaps and borders. */ public int getScrimColor() { return mScrimColor; } /** * Set the color of the scrim that is drawn over the contents, but under the flaps and borders. * * Use {@link android.graphics.Color#TRANSPARENT} to disable the scrim. */ public void setScrimColor(int color) { boolean changed = mScrimColor != color; mScrimColor = color; if (changed) { invalidateSelf(); } } /** * Sets whether we should work around an issue introduced in Android 4.4.3, * where a WebView can corrupt the stencil buffer of the canvas when the canvas is clipped * using a non-rectangular Path. */ public void setCompatibilityMode(boolean isCompatibilityMode) { boolean changed = mIsCompatibilityMode != isCompatibilityMode; mIsCompatibilityMode = isCompatibilityMode; if (changed) { invalidateSelf(); } } /** * Sets the color of the container that this drawable is in. The given color will be used in * {@link #setCompatibilityMode compatibility mode} to draw fake corners to emulate clipped * corners. */ public void setCompatibilityModeBackgroundColor(int color) { boolean changed = mCompatibilityModeBackgroundPaint.getColor() != color; mCompatibilityModeBackgroundPaint.setColor(color); if (changed) { invalidateSelf(); } } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); recalculatePath(); } /** * Override draw(android.graphics.Canvas) instead of * {@link #onDraw(android.graphics.Canvas)} to clip all the drawable layers. */ @Override public void draw(Canvas canvas) { final Rect bounds = getBounds(); if (bounds.isEmpty()) { return; } pauseInvalidate(); // Clip to path. if (!mIsCompatibilityMode) { canvas.save(); canvas.clipPath(mClipPath); } // Draw parent within path. super.draw(canvas); // Draw scrim on top of parent. canvas.drawColor(mScrimColor); // Draw flaps. float left = bounds.left + mBorderWidth / 2; float top = bounds.top + mBorderWidth / 2; float right = bounds.right - mBorderWidth / 2; float bottom = bounds.bottom - mBorderWidth / 2; RectF flapCornerRectF = sRectF; flapCornerRectF.set(0, 0, mCornerFlapSide + mCornerRoundRadius, mCornerFlapSide + mCornerRoundRadius); if (mTopLeftCornerStyle == CORNER_STYLE_FLAP) { flapCornerRectF.offsetTo(left, top); canvas.drawRoundRect(flapCornerRectF, mCornerRoundRadius, mCornerRoundRadius, mFlapPaint); } if (mTopRightCornerStyle == CORNER_STYLE_FLAP) { flapCornerRectF.offsetTo(right - mCornerFlapSide, top); canvas.drawRoundRect(flapCornerRectF, mCornerRoundRadius, mCornerRoundRadius, mFlapPaint); } if (mBottomRightCornerStyle == CORNER_STYLE_FLAP) { flapCornerRectF.offsetTo(right - mCornerFlapSide, bottom - mCornerFlapSide); canvas.drawRoundRect(flapCornerRectF, mCornerRoundRadius, mCornerRoundRadius, mFlapPaint); } if (mBottomLeftCornerStyle == CORNER_STYLE_FLAP) { flapCornerRectF.offsetTo(left, bottom - mCornerFlapSide); canvas.drawRoundRect(flapCornerRectF, mCornerRoundRadius, mCornerRoundRadius, mFlapPaint); } if (!mIsCompatibilityMode) { canvas.restore(); } if (mIsCompatibilityMode) { drawFakeCornersForCompatibilityMode(canvas); } // Draw border around path. canvas.drawPath(mClipPath, mBorderPaint); resumeInvalidate(); } @Override public void invalidateSelf() { if (!mEatInvalidates) { super.invalidateSelf(); } else { Log.d(TAG, "Skipping invalidate."); } } protected void drawFakeCornersForCompatibilityMode(final Canvas canvas) { final Rect bounds = getBounds(); float left = bounds.left; float top = bounds.top; float right = bounds.right; float bottom = bounds.bottom; // Draw fake round corners. RectF fakeCornerRectF = sRectF; fakeCornerRectF.set(0, 0, mCornerRoundRadius * 2, mCornerRoundRadius * 2); if (mTopLeftCornerStyle == CORNER_STYLE_ROUND) { fakeCornerRectF.offsetTo(left, top); mCompatibilityModePath.rewind(); mCompatibilityModePath.moveTo(left, top); mCompatibilityModePath.lineTo(left + mCornerRoundRadius, top); mCompatibilityModePath.arcTo(fakeCornerRectF, START_TOP, -QUARTER_CIRCLE); mCompatibilityModePath.close(); canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); } if (mTopRightCornerStyle == CORNER_STYLE_ROUND) { fakeCornerRectF.offsetTo(right - fakeCornerRectF.width(), top); mCompatibilityModePath.rewind(); mCompatibilityModePath.moveTo(right, top); mCompatibilityModePath.lineTo(right, top + mCornerRoundRadius); mCompatibilityModePath.arcTo(fakeCornerRectF, START_RIGHT, -QUARTER_CIRCLE); mCompatibilityModePath.close(); canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); } if (mBottomRightCornerStyle == CORNER_STYLE_ROUND) { fakeCornerRectF .offsetTo(right - fakeCornerRectF.width(), bottom - fakeCornerRectF.height()); mCompatibilityModePath.rewind(); mCompatibilityModePath.moveTo(right, bottom); mCompatibilityModePath.lineTo(right - mCornerRoundRadius, bottom); mCompatibilityModePath.arcTo(fakeCornerRectF, START_BOTTOM, -QUARTER_CIRCLE); mCompatibilityModePath.close(); canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); } if (mBottomLeftCornerStyle == CORNER_STYLE_ROUND) { fakeCornerRectF.offsetTo(left, bottom - fakeCornerRectF.height()); mCompatibilityModePath.rewind(); mCompatibilityModePath.moveTo(left, bottom); mCompatibilityModePath.lineTo(left, bottom - mCornerRoundRadius); mCompatibilityModePath.arcTo(fakeCornerRectF, START_LEFT, -QUARTER_CIRCLE); mCompatibilityModePath.close(); canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); } // Draw fake flap corners. if (mTopLeftCornerStyle == CORNER_STYLE_FLAP) { mCompatibilityModePath.rewind(); mCompatibilityModePath.moveTo(left, top); mCompatibilityModePath.lineTo(left + mCornerFlapSide, top); mCompatibilityModePath.lineTo(left, top + mCornerFlapSide); mCompatibilityModePath.close(); canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); } if (mTopRightCornerStyle == CORNER_STYLE_FLAP) { mCompatibilityModePath.rewind(); mCompatibilityModePath.moveTo(right, top); mCompatibilityModePath.lineTo(right, top + mCornerFlapSide); mCompatibilityModePath.lineTo(right - mCornerFlapSide, top); mCompatibilityModePath.close(); canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); } if (mBottomRightCornerStyle == CORNER_STYLE_FLAP) { mCompatibilityModePath.rewind(); mCompatibilityModePath.moveTo(right, bottom); mCompatibilityModePath.lineTo(right - mCornerFlapSide, bottom); mCompatibilityModePath.lineTo(right, bottom - mCornerFlapSide); mCompatibilityModePath.close(); canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); } if (mBottomLeftCornerStyle == CORNER_STYLE_FLAP) { mCompatibilityModePath.rewind(); mCompatibilityModePath.moveTo(left, bottom); mCompatibilityModePath.lineTo(left, bottom - mCornerFlapSide); mCompatibilityModePath.lineTo(left + mCornerFlapSide, bottom); mCompatibilityModePath.close(); canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); } } private void pauseInvalidate() { mEatInvalidates = true; } private void resumeInvalidate() { mEatInvalidates = false; } private void recalculatePath() { Rect bounds = getBounds(); if (bounds.isEmpty()) { return; } // Setup. float left = bounds.left + mBorderWidth / 2; float top = bounds.top + mBorderWidth / 2; float right = bounds.right - mBorderWidth / 2; float bottom = bounds.bottom - mBorderWidth / 2; RectF roundedCornerRectF = sRectF; roundedCornerRectF.set(0, 0, 2 * mCornerRoundRadius, 2 * mCornerRoundRadius); mClipPath.rewind(); switch (mTopLeftCornerStyle) { case CORNER_STYLE_SHARP: mClipPath.moveTo(left, top); break; case CORNER_STYLE_ROUND: roundedCornerRectF.offsetTo(left, top); mClipPath.arcTo(roundedCornerRectF, START_LEFT, QUARTER_CIRCLE); break; case CORNER_STYLE_FLAP: mClipPath.moveTo(left, top - mCornerFlapSide); mClipPath.lineTo(left + mCornerFlapSide, top); break; } switch (mTopRightCornerStyle) { case CORNER_STYLE_SHARP: mClipPath.lineTo(right, top); break; case CORNER_STYLE_ROUND: roundedCornerRectF.offsetTo(right - roundedCornerRectF.width(), top); mClipPath.arcTo(roundedCornerRectF, START_TOP, QUARTER_CIRCLE); break; case CORNER_STYLE_FLAP: mClipPath.lineTo(right - mCornerFlapSide, top); mClipPath.lineTo(right, top + mCornerFlapSide); break; } switch (mBottomRightCornerStyle) { case CORNER_STYLE_SHARP: mClipPath.lineTo(right, bottom); break; case CORNER_STYLE_ROUND: roundedCornerRectF.offsetTo(right - roundedCornerRectF.width(), bottom - roundedCornerRectF.height()); mClipPath.arcTo(roundedCornerRectF, START_RIGHT, QUARTER_CIRCLE); break; case CORNER_STYLE_FLAP: mClipPath.lineTo(right, bottom - mCornerFlapSide); mClipPath.lineTo(right - mCornerFlapSide, bottom); break; } switch (mBottomLeftCornerStyle) { case CORNER_STYLE_SHARP: mClipPath.lineTo(left, bottom); break; case CORNER_STYLE_ROUND: roundedCornerRectF.offsetTo(left, bottom - roundedCornerRectF.height()); mClipPath.arcTo(roundedCornerRectF, START_BOTTOM, QUARTER_CIRCLE); break; case CORNER_STYLE_FLAP: mClipPath.lineTo(left + mCornerFlapSide, bottom); mClipPath.lineTo(left, bottom - mCornerFlapSide); break; } // Finish. mClipPath.close(); } private void resolveCornerStyles() { boolean isLtr = getLayoutDirectionLocal() == View.LAYOUT_DIRECTION_LTR; setCornerStyles( isLtr ? mTopStartCornerStyle : mTopEndCornerStyle, isLtr ? mTopEndCornerStyle : mTopStartCornerStyle, isLtr ? mBottomEndCornerStyle : mBottomStartCornerStyle, isLtr ? mBottomStartCornerStyle : mBottomEndCornerStyle); } /** Set the corner styles for all four corners */ private void setCornerStyles(int topLeft, int topRight, int bottomRight, int bottomLeft) { boolean changed = mTopLeftCornerStyle != topLeft || mTopRightCornerStyle != topRight || mBottomRightCornerStyle != bottomRight || mBottomLeftCornerStyle != bottomLeft; mTopLeftCornerStyle = topLeft; mTopRightCornerStyle = topRight; mBottomRightCornerStyle = bottomRight; mBottomLeftCornerStyle = bottomLeft; if (changed) { recalculatePath(); } } }