1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 package com.android.systemui.recents.views; 17 18 import android.content.res.Resources; 19 import android.graphics.Canvas; 20 import android.graphics.ColorFilter; 21 import android.graphics.LinearGradient; 22 import android.graphics.Paint; 23 import android.graphics.Path; 24 import android.graphics.PixelFormat; 25 import android.graphics.RadialGradient; 26 import android.graphics.Rect; 27 import android.graphics.RectF; 28 import android.graphics.Shader; 29 import android.graphics.drawable.Drawable; 30 import android.util.Log; 31 32 import com.android.systemui.R; 33 import com.android.systemui.recents.RecentsConfiguration; 34 35 /** 36 * A rounded rectangle drawable which also includes a shadow around. This is mostly copied from 37 * frameworks/support/v7/cardview/eclair-mr1/android/support/v7/widget/ 38 * RoundRectDrawableWithShadow.java revision c42ba8c000d1e6ce85e152dfc17089a0a69e739f with a few 39 * modifications to suit our needs in SystemUI. 40 */ 41 class FakeShadowDrawable extends Drawable { 42 // used to calculate content padding 43 final static double COS_45 = Math.cos(Math.toRadians(45)); 44 45 final static float SHADOW_MULTIPLIER = 1.5f; 46 47 final float mInsetShadow; // extra shadow to avoid gaps between card and shadow 48 49 Paint mCornerShadowPaint; 50 51 Paint mEdgeShadowPaint; 52 53 final RectF mCardBounds; 54 55 float mCornerRadius; 56 57 Path mCornerShadowPath; 58 59 // updated value with inset 60 float mMaxShadowSize; 61 62 // actual value set by developer 63 float mRawMaxShadowSize; 64 65 // multiplied value to account for shadow offset 66 float mShadowSize; 67 68 // actual value set by developer 69 float mRawShadowSize; 70 71 private boolean mDirty = true; 72 73 private final int mShadowStartColor; 74 75 private final int mShadowEndColor; 76 77 private boolean mAddPaddingForCorners = true; 78 79 /** 80 * If shadow size is set to a value above max shadow, we print a warning 81 */ 82 private boolean mPrintedShadowClipWarning = false; 83 84 public FakeShadowDrawable(Resources resources, RecentsConfiguration config) { 85 mShadowStartColor = resources.getColor(R.color.fake_shadow_start_color); 86 mShadowEndColor = resources.getColor(R.color.fake_shadow_end_color); 87 mInsetShadow = resources.getDimension(R.dimen.fake_shadow_inset); 88 setShadowSize(resources.getDimensionPixelSize(R.dimen.fake_shadow_size), 89 resources.getDimensionPixelSize(R.dimen.fake_shadow_size)); 90 mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); 91 mCornerShadowPaint.setStyle(Paint.Style.FILL); 92 mCornerShadowPaint.setDither(true); 93 mCornerRadius = config.taskViewRoundedCornerRadiusPx; 94 mCardBounds = new RectF(); 95 mEdgeShadowPaint = new Paint(mCornerShadowPaint); 96 } 97 98 @Override 99 public void setAlpha(int alpha) { 100 mCornerShadowPaint.setAlpha(alpha); 101 mEdgeShadowPaint.setAlpha(alpha); 102 } 103 104 @Override 105 protected void onBoundsChange(Rect bounds) { 106 super.onBoundsChange(bounds); 107 mDirty = true; 108 } 109 110 void setShadowSize(float shadowSize, float maxShadowSize) { 111 if (shadowSize < 0 || maxShadowSize < 0) { 112 throw new IllegalArgumentException("invalid shadow size"); 113 } 114 if (shadowSize > maxShadowSize) { 115 shadowSize = maxShadowSize; 116 if (!mPrintedShadowClipWarning) { 117 Log.w("CardView", "Shadow size is being clipped by the max shadow size. See " 118 + "{CardView#setMaxCardElevation}."); 119 mPrintedShadowClipWarning = true; 120 } 121 } 122 if (mRawShadowSize == shadowSize && mRawMaxShadowSize == maxShadowSize) { 123 return; 124 } 125 mRawShadowSize = shadowSize; 126 mRawMaxShadowSize = maxShadowSize; 127 mShadowSize = shadowSize * SHADOW_MULTIPLIER + mInsetShadow; 128 mMaxShadowSize = maxShadowSize + mInsetShadow; 129 mDirty = true; 130 invalidateSelf(); 131 } 132 133 @Override 134 public boolean getPadding(Rect padding) { 135 int vOffset = (int) Math.ceil(calculateVerticalPadding(mRawMaxShadowSize, mCornerRadius, 136 mAddPaddingForCorners)); 137 int hOffset = (int) Math.ceil(calculateHorizontalPadding(mRawMaxShadowSize, mCornerRadius, 138 mAddPaddingForCorners)); 139 padding.set(hOffset, vOffset, hOffset, vOffset); 140 return true; 141 } 142 143 static float calculateVerticalPadding(float maxShadowSize, float cornerRadius, 144 boolean addPaddingForCorners) { 145 if (addPaddingForCorners) { 146 return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius); 147 } else { 148 return maxShadowSize * SHADOW_MULTIPLIER; 149 } 150 } 151 152 static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius, 153 boolean addPaddingForCorners) { 154 if (addPaddingForCorners) { 155 return (float) (maxShadowSize + (1 - COS_45) * cornerRadius); 156 } else { 157 return maxShadowSize; 158 } 159 } 160 161 @Override 162 public void setColorFilter(ColorFilter colorFilter) { 163 mCornerShadowPaint.setColorFilter(colorFilter); 164 mEdgeShadowPaint.setColorFilter(colorFilter); 165 } 166 167 @Override 168 public int getOpacity() { 169 return PixelFormat.OPAQUE; 170 } 171 172 @Override 173 public void draw(Canvas canvas) { 174 if (mDirty) { 175 buildComponents(getBounds()); 176 mDirty = false; 177 } 178 canvas.translate(0, mRawShadowSize / 4); 179 drawShadow(canvas); 180 canvas.translate(0, -mRawShadowSize / 4); 181 } 182 183 private void drawShadow(Canvas canvas) { 184 final float edgeShadowTop = -mCornerRadius - mShadowSize; 185 final float inset = mCornerRadius + mInsetShadow + mRawShadowSize / 2; 186 final boolean drawHorizontalEdges = mCardBounds.width() - 2 * inset > 0; 187 final boolean drawVerticalEdges = mCardBounds.height() - 2 * inset > 0; 188 // LT 189 int saved = canvas.save(); 190 canvas.translate(mCardBounds.left + inset, mCardBounds.top + inset); 191 canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); 192 if (drawHorizontalEdges) { 193 canvas.drawRect(0, edgeShadowTop, 194 mCardBounds.width() - 2 * inset, -mCornerRadius, 195 mEdgeShadowPaint); 196 } 197 canvas.restoreToCount(saved); 198 // RB 199 saved = canvas.save(); 200 canvas.translate(mCardBounds.right - inset, mCardBounds.bottom - inset); 201 canvas.rotate(180f); 202 canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); 203 if (drawHorizontalEdges) { 204 canvas.drawRect(0, edgeShadowTop, 205 mCardBounds.width() - 2 * inset, -mCornerRadius + mShadowSize, 206 mEdgeShadowPaint); 207 } 208 canvas.restoreToCount(saved); 209 // LB 210 saved = canvas.save(); 211 canvas.translate(mCardBounds.left + inset, mCardBounds.bottom - inset); 212 canvas.rotate(270f); 213 canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); 214 if (drawVerticalEdges) { 215 canvas.drawRect(0, edgeShadowTop, 216 mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint); 217 } 218 canvas.restoreToCount(saved); 219 // RT 220 saved = canvas.save(); 221 canvas.translate(mCardBounds.right - inset, mCardBounds.top + inset); 222 canvas.rotate(90f); 223 canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); 224 if (drawVerticalEdges) { 225 canvas.drawRect(0, edgeShadowTop, 226 mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint); 227 } 228 canvas.restoreToCount(saved); 229 } 230 231 private void buildShadowCorners() { 232 RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius); 233 RectF outerBounds = new RectF(innerBounds); 234 outerBounds.inset(-mShadowSize, -mShadowSize); 235 236 if (mCornerShadowPath == null) { 237 mCornerShadowPath = new Path(); 238 } else { 239 mCornerShadowPath.reset(); 240 } 241 mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD); 242 mCornerShadowPath.moveTo(-mCornerRadius, 0); 243 mCornerShadowPath.rLineTo(-mShadowSize, 0); 244 // outer arc 245 mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false); 246 // inner arc 247 mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false); 248 mCornerShadowPath.close(); 249 250 float startRatio = mCornerRadius / (mCornerRadius + mShadowSize); 251 mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize, 252 new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor}, 253 new float[]{0f, startRatio, 1f} 254 , Shader.TileMode.CLAMP)); 255 256 // we offset the content shadowSize/2 pixels up to make it more realistic. 257 // this is why edge shadow shader has some extra space 258 // When drawing bottom edge shadow, we use that extra space. 259 mEdgeShadowPaint.setShader(new LinearGradient(0, -mCornerRadius + mShadowSize, 0, 260 -mCornerRadius - mShadowSize, 261 new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor}, 262 new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP)); 263 } 264 265 private void buildComponents(Rect bounds) { 266 // Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift. 267 // We could have different top-bottom offsets to avoid extra gap above but in that case 268 // center aligning Views inside the CardView would be problematic. 269 final float verticalOffset = mMaxShadowSize * SHADOW_MULTIPLIER; 270 mCardBounds.set(bounds.left + mMaxShadowSize, bounds.top + verticalOffset, 271 bounds.right - mMaxShadowSize, bounds.bottom - verticalOffset); 272 buildShadowCorners(); 273 } 274 275 float getMinWidth() { 276 final float content = 2 * 277 Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow + mRawMaxShadowSize / 2); 278 return content + (mRawMaxShadowSize + mInsetShadow) * 2; 279 } 280 281 float getMinHeight() { 282 final float content = 2 * Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow 283 + mRawMaxShadowSize * SHADOW_MULTIPLIER / 2); 284 return content + (mRawMaxShadowSize * SHADOW_MULTIPLIER + mInsetShadow) * 2; 285 } 286 }