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 import android.view.math.Math3DHelper; 27 28 import static android.view.shadow.ShadowConstants.MIN_ALPHA; 29 import static android.view.shadow.ShadowConstants.SCALE_DOWN; 30 31 public class HighQualityShadowPainter { 32 private static final float sRoundedGap = (float) (1.0 - Math.sqrt(2.0) / 2.0); 33 HighQualityShadowPainter()34 private HighQualityShadowPainter() { } 35 36 /** 37 * Draws simple Rect shadow 38 */ paintRectShadow(ViewGroup parent, Outline outline, float elevation, Canvas canvas, float alpha, float densityDpi)39 public static void paintRectShadow(ViewGroup parent, Outline outline, float elevation, 40 Canvas canvas, float alpha, float densityDpi) { 41 42 if (!validate(elevation, densityDpi)) { 43 return; 44 } 45 46 int width = parent.getWidth() / SCALE_DOWN; 47 int height = parent.getHeight() / SCALE_DOWN; 48 49 Rect rectOriginal = new Rect(); 50 Rect rectScaled = new Rect(); 51 if (!outline.getRect(rectScaled) || alpha < MIN_ALPHA) { 52 // If alpha below MIN_ALPHA it's invisible (based on manual test). Save some perf. 53 return; 54 } 55 56 outline.getRect(rectOriginal); 57 58 rectScaled.left /= SCALE_DOWN; 59 rectScaled.right /= SCALE_DOWN; 60 rectScaled.top /= SCALE_DOWN; 61 rectScaled.bottom /= SCALE_DOWN; 62 float radius = outline.getRadius() / SCALE_DOWN; 63 64 if (radius > rectScaled.width() || radius > rectScaled.height()) { 65 // Rounded edge generation fails if radius is bigger than drawing box. 66 return; 67 } 68 69 // ensure alpha doesn't go over 1 70 alpha = (alpha > 1.0f) ? 1.0f : alpha; 71 boolean isOpaque = outline.getAlpha() * alpha == 1.0f; 72 float[] poly = getPoly(rectScaled, elevation / SCALE_DOWN, radius); 73 74 AmbientShadowConfig ambientConfig = new AmbientShadowConfig.Builder() 75 .setPolygon(poly) 76 .setLightSourcePosition( 77 (rectScaled.left + rectScaled.right) / 2.0f, 78 (rectScaled.top + rectScaled.bottom) / 2.0f) 79 .setEdgeScale(ShadowConstants.AMBIENT_SHADOW_EDGE_SCALE) 80 .setShadowBoundRatio(ShadowConstants.AMBIENT_SHADOW_SHADOW_BOUND) 81 .setShadowStrength(ShadowConstants.AMBIENT_SHADOW_STRENGTH * alpha) 82 .build(); 83 84 AmbientShadowTriangulator ambientTriangulator = new AmbientShadowTriangulator(ambientConfig); 85 ambientTriangulator.triangulate(); 86 87 SpotShadowTriangulator spotTriangulator = null; 88 float lightZHeightPx = ShadowConstants.SPOT_SHADOW_LIGHT_Z_HEIGHT_DP * (densityDpi / DisplayMetrics.DENSITY_DEFAULT); 89 if (lightZHeightPx - elevation / SCALE_DOWN >= ShadowConstants.SPOT_SHADOW_LIGHT_Z_EPSILON) { 90 91 float lightX = (rectScaled.left + rectScaled.right) / 2; 92 float lightY = rectScaled.top; 93 // Light shouldn't be bigger than the object by too much. 94 int dynamicLightRadius = Math.min(rectScaled.width(), rectScaled.height()); 95 96 SpotShadowConfig spotConfig = new SpotShadowConfig.Builder() 97 .setLightCoord(lightX, lightY, lightZHeightPx) 98 .setLightRadius(dynamicLightRadius) 99 .setShadowStrength(ShadowConstants.SPOT_SHADOW_STRENGTH * alpha) 100 .setPolygon(poly, poly.length / ShadowConstants.COORDINATE_SIZE) 101 .build(); 102 103 spotTriangulator = new SpotShadowTriangulator(spotConfig); 104 spotTriangulator.triangulate(); 105 } 106 107 int translateX = 0; 108 int translateY = 0; 109 int imgW = 0; 110 int imgH = 0; 111 112 if (ambientTriangulator.isValid()) { 113 float[] shadowBounds = Math3DHelper.flatBound(ambientTriangulator.getVertices(), 2); 114 // Move the shadow to the left top corner to occupy the least possible bitmap 115 116 translateX = -(int) Math.floor(shadowBounds[0]); 117 translateY = -(int) Math.floor(shadowBounds[1]); 118 119 // create bitmap of the least possible size that covers the entire shadow 120 imgW = (int) Math.ceil(shadowBounds[2] + translateX); 121 imgH = (int) Math.ceil(shadowBounds[3] + translateY); 122 } 123 124 if (spotTriangulator != null && spotTriangulator.validate()) { 125 126 // Bit of a hack to re-adjust spot shadow to fit correctly within parent canvas. 127 // Problem is that outline passed is not a final position, which throws off our 128 // whereas our shadow rendering algorithm, which requires pre-set range for 129 // optimization purposes. 130 float[] shadowBounds = Math3DHelper.flatBound(spotTriangulator.getStrips()[0], 3); 131 132 if ((shadowBounds[2] - shadowBounds[0]) > width || 133 (shadowBounds[3] - shadowBounds[1]) > height) { 134 // Spot shadow to be casted is larger than the parent canvas, 135 // We'll let ambient shadow do the trick and skip spot shadow here. 136 spotTriangulator = null; 137 } 138 139 translateX = Math.max(-(int) Math.floor(shadowBounds[0]), translateX); 140 translateY = Math.max(-(int) Math.floor(shadowBounds[1]), translateY); 141 142 // create bitmap of the least possible size that covers the entire shadow 143 imgW = Math.max((int) Math.ceil(shadowBounds[2] + translateX), imgW); 144 imgH = Math.max((int) Math.ceil(shadowBounds[3] + translateY), imgH); 145 } 146 147 TriangleBuffer renderer = new TriangleBuffer(); 148 renderer.setSize(imgW, imgH, 0); 149 150 if (ambientTriangulator.isValid()) { 151 152 Math3DHelper.translate(ambientTriangulator.getVertices(), translateX, translateY, 2); 153 renderer.drawTriangles(ambientTriangulator.getIndices(), ambientTriangulator.getVertices(), 154 ambientTriangulator.getColors(), ambientConfig.getShadowStrength()); 155 } 156 157 if (spotTriangulator != null && spotTriangulator.validate()) { 158 float[][] strips = spotTriangulator.getStrips(); 159 for (int i = 0; i < strips.length; ++i) { 160 Math3DHelper.translate(strips[i], translateX, translateY, 3); 161 renderer.drawTriangles(strips[i], ShadowConstants.SPOT_SHADOW_STRENGTH * alpha); 162 } 163 } 164 165 Bitmap img = renderer.createImage(); 166 167 drawScaled(canvas, img, translateX, translateY, rectOriginal, radius, isOpaque); 168 } 169 170 /** 171 * High quality shadow does not work well with object that is too high in elevation. Check if 172 * the object elevation is reasonable and returns true if shadow will work well. False other 173 * wise. 174 */ validate(float elevation, float densityDpi)175 private static boolean validate(float elevation, float densityDpi) { 176 float scaledElevationPx = elevation / SCALE_DOWN; 177 float scaledSpotLightHeightPx = ShadowConstants.SPOT_SHADOW_LIGHT_Z_HEIGHT_DP * 178 (densityDpi / DisplayMetrics.DENSITY_DEFAULT); 179 if (scaledElevationPx > scaledSpotLightHeightPx) { 180 return false; 181 } 182 183 return true; 184 } 185 186 /** 187 * Draw the bitmap scaled up. 188 * @param translateX - offset in x axis by which the bitmap is shifted. 189 * @param translateY - offset in y axis by which the bitmap is shifted. 190 * @param shadowCaster - unscaled outline of shadow caster 191 * @param radius 192 */ drawScaled(Canvas canvas, Bitmap bitmap, int translateX, int translateY, Rect shadowCaster, float radius, boolean isOpaque)193 private static void drawScaled(Canvas canvas, Bitmap bitmap, int translateX, int translateY, 194 Rect shadowCaster, float radius, boolean isOpaque) { 195 int unscaledTranslateX = translateX * SCALE_DOWN; 196 int unscaledTranslateY = translateY * SCALE_DOWN; 197 198 // To the canvas 199 Rect dest = new Rect( 200 -unscaledTranslateX, 201 -unscaledTranslateY, 202 (bitmap.getWidth() * SCALE_DOWN) - unscaledTranslateX, 203 (bitmap.getHeight() * SCALE_DOWN) - unscaledTranslateY); 204 Rect destSrc = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); 205 // We can skip drawing the shadows behind the caster if either 206 // 1) radius is 0, the shadow caster is rectangle and we can have a perfect cut 207 // 2) shadow caster is opaque and even if remove shadow only partially it won't affect 208 // the visual quality, otherwise we will observe shadow part through the translucent caster 209 // This can be improved by: 210 // TODO: do not draw the shadow behind the caster at all during the tesselation phase 211 if (radius > 0 && !isOpaque) { 212 // Rounded edge. 213 int save = canvas.save(); 214 canvas.drawBitmap(bitmap, destSrc, dest, null); 215 canvas.restoreToCount(save); 216 return; 217 } 218 219 /** 220 * ---------------------------------- 221 * | | 222 * | top | 223 * | | 224 * ---------------------------------- 225 * | | | | 226 * | left | shadow caster | right | 227 * | | | | 228 * ---------------------------------- 229 * | | 230 * | bottom | 231 * | | 232 * ---------------------------------- 233 * 234 * dest == top + left + shadow caster + right + bottom 235 * Visually, canvas.drawBitmap(bitmap, destSrc, dest, paint) would achieve the same result. 236 */ 237 int gap = (int) Math.ceil(radius * SCALE_DOWN * sRoundedGap); 238 shadowCaster.bottom -= gap; 239 shadowCaster.top += gap; 240 shadowCaster.left += gap; 241 shadowCaster.right -= gap; 242 Rect left = new Rect(dest.left, shadowCaster.top, shadowCaster.left, 243 shadowCaster.bottom); 244 int leftScaled = left.width() / SCALE_DOWN + destSrc.left; 245 246 Rect top = new Rect(dest.left, dest.top, dest.right, shadowCaster.top); 247 int topScaled = top.height() / SCALE_DOWN + destSrc.top; 248 249 Rect right = new Rect(shadowCaster.right, shadowCaster.top, dest.right, 250 shadowCaster.bottom); 251 int rightScaled = (shadowCaster.right - dest.left) / SCALE_DOWN + destSrc.left; 252 253 Rect bottom = new Rect(dest.left, shadowCaster.bottom, dest.right, dest.bottom); 254 int bottomScaled = (shadowCaster.bottom - dest.top) / SCALE_DOWN + destSrc.top; 255 256 // calculate parts of the middle ground that can be ignored. 257 Rect leftSrc = new Rect(destSrc.left, topScaled, leftScaled, bottomScaled); 258 Rect topSrc = new Rect(destSrc.left, destSrc.top, destSrc.right, topScaled); 259 Rect rightSrc = new Rect(rightScaled, topScaled, destSrc.right, bottomScaled); 260 Rect bottomSrc = new Rect(destSrc.left, bottomScaled, destSrc.right, destSrc.bottom); 261 262 int save = canvas.save(); 263 Paint paint = new Paint(); 264 canvas.drawBitmap(bitmap, leftSrc, left, paint); 265 canvas.drawBitmap(bitmap, topSrc, top, paint); 266 canvas.drawBitmap(bitmap, rightSrc, right, paint); 267 canvas.drawBitmap(bitmap, bottomSrc, bottom, paint); 268 canvas.restoreToCount(save); 269 } 270 getPoly(Rect rect, float elevation, float radius)271 private static float[] getPoly(Rect rect, float elevation, float radius) { 272 if (radius <= 0) { 273 float[] poly = new float[ShadowConstants.RECT_VERTICES_SIZE * ShadowConstants.COORDINATE_SIZE]; 274 275 poly[0] = poly[9] = rect.left; 276 poly[1] = poly[4] = rect.top; 277 poly[3] = poly[6] = rect.right; 278 poly[7] = poly[10] = rect.bottom; 279 poly[2] = poly[5] = poly[8] = poly[11] = elevation; 280 281 return poly; 282 } 283 284 return buildRoundedEdges(rect, elevation, radius); 285 } 286 buildRoundedEdges( Rect rect, float elevation, float radius)287 private static float[] buildRoundedEdges( 288 Rect rect, float elevation, float radius) { 289 290 float[] roundedEdgeVertices = new float[(ShadowConstants.SPLICE_ROUNDED_EDGE + 1) * 4 * 3]; 291 int index = 0; 292 // 1.0 LT. From theta 0 to pi/2 in K division. 293 for (int i = 0; i <= ShadowConstants.SPLICE_ROUNDED_EDGE; i++) { 294 double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE); 295 float x = (float) (rect.left + (radius - radius * Math.cos(theta))); 296 float y = (float) (rect.top + (radius - radius * Math.sin(theta))); 297 roundedEdgeVertices[index++] = x; 298 roundedEdgeVertices[index++] = y; 299 roundedEdgeVertices[index++] = elevation; 300 } 301 302 // 2.0 RT 303 for (int i = ShadowConstants.SPLICE_ROUNDED_EDGE; i >= 0; i--) { 304 double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE); 305 float x = (float) (rect.right - (radius - radius * Math.cos(theta))); 306 float y = (float) (rect.top + (radius - radius * Math.sin(theta))); 307 roundedEdgeVertices[index++] = x; 308 roundedEdgeVertices[index++] = y; 309 roundedEdgeVertices[index++] = elevation; 310 } 311 312 // 3.0 RB 313 for (int i = 0; i <= ShadowConstants.SPLICE_ROUNDED_EDGE; i++) { 314 double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE); 315 float x = (float) (rect.right - (radius - radius * Math.cos(theta))); 316 float y = (float) (rect.bottom - (radius - radius * Math.sin(theta))); 317 roundedEdgeVertices[index++] = x; 318 roundedEdgeVertices[index++] = y; 319 roundedEdgeVertices[index++] = elevation; 320 } 321 322 // 4.0 LB 323 for (int i = ShadowConstants.SPLICE_ROUNDED_EDGE; i >= 0; i--) { 324 double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE); 325 float x = (float) (rect.left + (radius - radius * Math.cos(theta))); 326 float y = (float) (rect.bottom - (radius - radius * Math.sin(theta))); 327 roundedEdgeVertices[index++] = x; 328 roundedEdgeVertices[index++] = y; 329 roundedEdgeVertices[index++] = elevation; 330 } 331 332 return roundedEdgeVertices; 333 } 334 } 335