1 /* 2 * Copyright (C) 2018 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 17 package android.view.shadow; 18 19 import android.graphics.Bitmap; 20 import android.graphics.Canvas; 21 import android.graphics.Outline; 22 import android.graphics.Paint; 23 import android.graphics.Rect; 24 import android.util.DisplayMetrics; 25 import android.view.ViewGroup; 26 27 import static android.view.shadow.ShadowConstants.MIN_ALPHA; 28 import static android.view.shadow.ShadowConstants.SCALE_DOWN; 29 30 public class HighQualityShadowPainter { 31 HighQualityShadowPainter()32 private HighQualityShadowPainter() { } 33 34 /** 35 * Draws simple Rect shadow 36 */ paintRectShadow(ViewGroup parent, Outline outline, float elevation, Canvas canvas, float alpha, float densityDpi)37 public static void paintRectShadow(ViewGroup parent, Outline outline, float elevation, 38 Canvas canvas, float alpha, float densityDpi) { 39 40 if (!validate(elevation, densityDpi)) { 41 return; 42 } 43 44 int width = parent.getWidth() / SCALE_DOWN; 45 int height = parent.getHeight() / SCALE_DOWN; 46 47 Rect rectOriginal = new Rect(); 48 Rect rectScaled = new Rect(); 49 if (!outline.getRect(rectScaled) || alpha < MIN_ALPHA) { 50 // If alpha below MIN_ALPHA it's invisible (based on manual test). Save some perf. 51 return; 52 } 53 54 outline.getRect(rectOriginal); 55 56 rectScaled.left /= SCALE_DOWN; 57 rectScaled.right /= SCALE_DOWN; 58 rectScaled.top /= SCALE_DOWN; 59 rectScaled.bottom /= SCALE_DOWN; 60 float radius = outline.getRadius() / SCALE_DOWN; 61 62 if (radius > rectScaled.width() || radius > rectScaled.height()) { 63 // Rounded edge generation fails if radius is bigger than drawing box. 64 return; 65 } 66 67 // ensure alpha doesn't go over 1 68 alpha = (alpha > 1.0f) ? 1.0f : alpha; 69 float[] poly = getPoly(rectScaled, elevation / SCALE_DOWN, radius); 70 71 paintAmbientShadow(poly, canvas, width, height, alpha, rectOriginal, radius); 72 paintSpotShadow(poly, rectScaled, elevation / SCALE_DOWN, 73 canvas, densityDpi, width, height, alpha, rectOriginal, radius); 74 } 75 76 /** 77 * High quality shadow does not work well with object that is too high in elevation. Check if 78 * the object elevation is reasonable and returns true if shadow will work well. False other 79 * wise. 80 */ validate(float elevation, float densityDpi)81 private static boolean validate(float elevation, float densityDpi) { 82 float scaledElevationPx = elevation / SCALE_DOWN; 83 float scaledSpotLightHeightPx = ShadowConstants.SPOT_SHADOW_LIGHT_Z_HEIGHT_DP * 84 (densityDpi / DisplayMetrics.DENSITY_DEFAULT); 85 if (scaledElevationPx > scaledSpotLightHeightPx) { 86 return false; 87 } 88 89 return true; 90 } 91 92 /** 93 * @param polygon - polygon of the shadow caster 94 * @param canvas - canvas to draw 95 * @param width - scaled canvas (parent) width 96 * @param height - scaled canvas (parent) height 97 * @param alpha - 0-1 scale 98 * @param shadowCasterOutline - unscaled original shadow caster outline. 99 * @param radius 100 */ paintAmbientShadow(float[] polygon, Canvas canvas, int width, int height, float alpha, Rect shadowCasterOutline, float radius)101 private static void paintAmbientShadow(float[] polygon, Canvas canvas, int width, int height, 102 float alpha, Rect shadowCasterOutline, float radius) { 103 // TODO: Consider re-using the triangle buffer here since the world stays consistent. 104 // TODO: Reduce the buffer size based on shadow bounds. 105 106 AmbientShadowConfig config = new AmbientShadowConfig.Builder() 107 .setSize(width, height) 108 .setPolygon(polygon) 109 .setEdgeScale(ShadowConstants.AMBIENT_SHADOW_EDGE_SCALE) 110 .setShadowBoundRatio(ShadowConstants.AMBIENT_SHADOW_SHADOW_BOUND) 111 .setShadowStrength(ShadowConstants.AMBIENT_SHADOW_STRENGTH * alpha) 112 .setRays(ShadowConstants.AMBIENT_SHADOW_RAYS) 113 .setLayers(ShadowConstants.AMBIENT_SHADOW_LAYERS) 114 .build(); 115 116 AmbientShadowBitmapGenerator generator = new AmbientShadowBitmapGenerator(config); 117 generator.populateShadow(); 118 119 if (!generator.isValid()) { 120 return; 121 } 122 123 drawScaled( 124 canvas, generator.getBitmap(), (int) generator.getTranslateX(), 125 (int) generator.getTranslateY(), width, height, 126 shadowCasterOutline, radius); 127 } 128 129 /** 130 * @param poly - polygon of the shadow caster 131 * @param rectBound - scaled bounds of shadow caster. 132 * @param canvas - canvas to draw 133 * @param width - scaled canvas (parent) width 134 * @param height - scaled canvas (parent) height 135 * @param alpha - 0-1 scale 136 * @param shadowCasterOutline - unscaled original shadow caster outline. 137 * @param radius 138 */ paintSpotShadow(float[] poly, Rect rectBound, float elevation, Canvas canvas, float densityDpi, int width, int height, float alpha, Rect shadowCasterOutline, float radius)139 private static void paintSpotShadow(float[] poly, Rect rectBound, float elevation, Canvas canvas, 140 float densityDpi, int width, int height, float alpha, Rect shadowCasterOutline, 141 float radius) { 142 143 // TODO: Use alpha later 144 float lightZHeightPx = ShadowConstants.SPOT_SHADOW_LIGHT_Z_HEIGHT_DP * (densityDpi / DisplayMetrics.DENSITY_DEFAULT); 145 if (lightZHeightPx - elevation < ShadowConstants.SPOT_SHADOW_LIGHT_Z_EPSILON) { 146 // If the view is above or too close to the light source then return. 147 // This is done to somewhat simulate android behaviour. 148 return; 149 } 150 151 float lightX = (rectBound.left + rectBound.right) / 2; 152 float lightY = rectBound.top; 153 // Light shouldn't be bigger than the object by too much. 154 int dynamicLightRadius = Math.min(rectBound.width(), rectBound.height()); 155 156 SpotShadowConfig config = new SpotShadowConfig.Builder() 157 .setSize(width, height) 158 .setLayers(ShadowConstants.SPOT_SHADOW_LAYERS) 159 .setRays(ShadowConstants.SPOT_SHADOW_RAYS) 160 .setLightCoord(lightX, lightY, lightZHeightPx) 161 .setLightRadius(dynamicLightRadius) 162 .setLightSourcePoints(ShadowConstants.SPOT_SHADOW_LIGHT_SOURCE_POINTS) 163 .setShadowStrength(ShadowConstants.SPOT_SHADOW_STRENGTH * alpha) 164 .setPolygon(poly, poly.length / ShadowConstants.COORDINATE_SIZE) 165 .build(); 166 167 SpotShadowBitmapGenerator generator = new SpotShadowBitmapGenerator(config); 168 generator.populateShadow(); 169 170 if (!generator.validate()) { 171 return; 172 } 173 174 drawScaled(canvas, generator.getBitmap(), (int) generator.getTranslateX(), 175 (int) generator.getTranslateY(), width, height, shadowCasterOutline, radius); 176 } 177 178 /** 179 * Draw the bitmap scaled up. 180 * @param translateX - offset in x axis by which the bitmap is shifted. 181 * @param translateY - offset in y axis by which the bitmap is shifted. 182 * @param width - scaled width of canvas (parent) 183 * @param height - scaled height of canvas (parent) 184 * @param shadowCaster - unscaled outline of shadow caster 185 * @param radius 186 */ drawScaled(Canvas canvas, Bitmap bitmap, int translateX, int translateY, int width, int height, Rect shadowCaster, float radius)187 private static void drawScaled(Canvas canvas, Bitmap bitmap, int translateX, int translateY, 188 int width, int height, Rect shadowCaster, float radius) { 189 int unscaledTranslateX = translateX * SCALE_DOWN; 190 int unscaledTranslateY = translateY * SCALE_DOWN; 191 192 // To the canvas 193 Rect dest = new Rect( 194 -unscaledTranslateX, 195 -unscaledTranslateY, 196 (width * SCALE_DOWN) - unscaledTranslateX, 197 (height * SCALE_DOWN) - unscaledTranslateY); 198 Rect destSrc = new Rect(0, 0, width, height); 199 200 if (radius > 0) { 201 // Rounded edge. 202 int save = canvas.save(); 203 canvas.drawBitmap(bitmap, destSrc, dest, null); 204 canvas.restoreToCount(save); 205 return; 206 } 207 208 /** 209 * ---------------------------------- 210 * | | 211 * | top | 212 * | | 213 * ---------------------------------- 214 * | | | | 215 * | left | shadow caster | right | 216 * | | | | 217 * ---------------------------------- 218 * | | 219 * | bottom | 220 * | | 221 * ---------------------------------- 222 * 223 * dest == top + left + shadow caster + right + bottom 224 * Visually, canvas.drawBitmap(bitmap, destSrc, dest, paint) would achieve the same result. 225 */ 226 Rect left = new Rect(dest.left, shadowCaster.top, shadowCaster.left, shadowCaster.bottom); 227 int leftScaled = left.width() / SCALE_DOWN + destSrc.left; 228 229 Rect top = new Rect(dest.left, dest.top, dest.right, shadowCaster.top); 230 int topScaled = top.height() / SCALE_DOWN + destSrc.top; 231 232 Rect right = new Rect(shadowCaster.right, shadowCaster.top, dest.right, 233 shadowCaster.bottom); 234 int rightScaled = (shadowCaster.right + unscaledTranslateX) / SCALE_DOWN + destSrc.left; 235 236 Rect bottom = new Rect(dest.left, shadowCaster.bottom, dest.right, dest.bottom); 237 int bottomScaled = (bottom.bottom - bottom.height()) / SCALE_DOWN + destSrc.top; 238 239 // calculate parts of the middle ground that can be ignored. 240 Rect leftSrc = new Rect(destSrc.left, topScaled, leftScaled, bottomScaled); 241 Rect topSrc = new Rect(destSrc.left, destSrc.top, destSrc.right, topScaled); 242 Rect rightSrc = new Rect(rightScaled, topScaled, destSrc.right, bottomScaled); 243 Rect bottomSrc = new Rect(destSrc.left, bottomScaled, destSrc.right, destSrc.bottom); 244 245 int save = canvas.save(); 246 Paint paint = new Paint(); 247 canvas.drawBitmap(bitmap, leftSrc, left, paint); 248 canvas.drawBitmap(bitmap, topSrc, top, paint); 249 canvas.drawBitmap(bitmap, rightSrc, right, paint); 250 canvas.drawBitmap(bitmap, bottomSrc, bottom, paint); 251 canvas.restoreToCount(save); 252 } 253 getPoly(Rect rect, float elevation, float radius)254 private static float[] getPoly(Rect rect, float elevation, float radius) { 255 if (radius <= 0) { 256 float[] poly = new float[ShadowConstants.RECT_VERTICES_SIZE * ShadowConstants.COORDINATE_SIZE]; 257 258 poly[0] = poly[9] = rect.left; 259 poly[1] = poly[4] = rect.top; 260 poly[3] = poly[6] = rect.right; 261 poly[7] = poly[10] = rect.bottom; 262 poly[2] = poly[5] = poly[8] = poly[11] = elevation; 263 264 return poly; 265 } 266 267 return buildRoundedEdges(rect, elevation, radius); 268 } 269 buildRoundedEdges( Rect rect, float elevation, float radius)270 private static float[] buildRoundedEdges( 271 Rect rect, float elevation, float radius) { 272 273 float[] roundedEdgeVertices = new float[(ShadowConstants.SPLICE_ROUNDED_EDGE + 1) * 4 * 3]; 274 int index = 0; 275 // 1.0 LT. From theta 0 to pi/2 in K division. 276 for (int i = 0; i <= ShadowConstants.SPLICE_ROUNDED_EDGE; i++) { 277 double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE); 278 float x = (float) (rect.left + (radius - radius * Math.cos(theta))); 279 float y = (float) (rect.top + (radius - radius * Math.sin(theta))); 280 roundedEdgeVertices[index++] = x; 281 roundedEdgeVertices[index++] = y; 282 roundedEdgeVertices[index++] = elevation; 283 } 284 285 // 2.0 RT 286 for (int i = ShadowConstants.SPLICE_ROUNDED_EDGE; i >= 0; i--) { 287 double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE); 288 float x = (float) (rect.right - (radius - radius * Math.cos(theta))); 289 float y = (float) (rect.top + (radius - radius * Math.sin(theta))); 290 roundedEdgeVertices[index++] = x; 291 roundedEdgeVertices[index++] = y; 292 roundedEdgeVertices[index++] = elevation; 293 } 294 295 // 3.0 RB 296 for (int i = 0; i <= ShadowConstants.SPLICE_ROUNDED_EDGE; i++) { 297 double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE); 298 float x = (float) (rect.right - (radius - radius * Math.cos(theta))); 299 float y = (float) (rect.bottom - (radius - radius * Math.sin(theta))); 300 roundedEdgeVertices[index++] = x; 301 roundedEdgeVertices[index++] = y; 302 roundedEdgeVertices[index++] = elevation; 303 } 304 305 // 4.0 LB 306 for (int i = ShadowConstants.SPLICE_ROUNDED_EDGE; i >= 0; i--) { 307 double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE); 308 float x = (float) (rect.left + (radius - radius * Math.cos(theta))); 309 float y = (float) (rect.bottom - (radius - radius * Math.sin(theta))); 310 roundedEdgeVertices[index++] = x; 311 roundedEdgeVertices[index++] = y; 312 roundedEdgeVertices[index++] = elevation; 313 } 314 315 return roundedEdgeVertices; 316 } 317 } 318