| /* |
| * 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(); |
| } |
| } |
| } |