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