| /* |
| * Copyright (C) 2018 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 android.view.shadow; |
| |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Outline; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.util.DisplayMetrics; |
| import android.view.ViewGroup; |
| import android.view.math.Math3DHelper; |
| |
| import static android.view.shadow.ShadowConstants.MIN_ALPHA; |
| import static android.view.shadow.ShadowConstants.SCALE_DOWN; |
| |
| public class HighQualityShadowPainter { |
| private static final float sRoundedGap = (float) (1.0 - Math.sqrt(2.0) / 2.0); |
| |
| private HighQualityShadowPainter() { } |
| |
| /** |
| * Draws simple Rect shadow |
| */ |
| public static void paintRectShadow(ViewGroup parent, Outline outline, float elevation, |
| Canvas canvas, float alpha, float densityDpi) { |
| |
| if (!validate(elevation, densityDpi)) { |
| return; |
| } |
| |
| int width = parent.getWidth() / SCALE_DOWN; |
| int height = parent.getHeight() / SCALE_DOWN; |
| |
| Rect rectOriginal = new Rect(); |
| Rect rectScaled = new Rect(); |
| if (!outline.getRect(rectScaled) || alpha < MIN_ALPHA) { |
| // If alpha below MIN_ALPHA it's invisible (based on manual test). Save some perf. |
| return; |
| } |
| |
| outline.getRect(rectOriginal); |
| |
| rectScaled.left /= SCALE_DOWN; |
| rectScaled.right /= SCALE_DOWN; |
| rectScaled.top /= SCALE_DOWN; |
| rectScaled.bottom /= SCALE_DOWN; |
| float radius = outline.getRadius() / SCALE_DOWN; |
| |
| if (radius > rectScaled.width() || radius > rectScaled.height()) { |
| // Rounded edge generation fails if radius is bigger than drawing box. |
| return; |
| } |
| |
| // ensure alpha doesn't go over 1 |
| alpha = (alpha > 1.0f) ? 1.0f : alpha; |
| boolean isOpaque = outline.getAlpha() * alpha == 1.0f; |
| float[] poly = getPoly(rectScaled, elevation / SCALE_DOWN, radius); |
| |
| AmbientShadowConfig ambientConfig = new AmbientShadowConfig.Builder() |
| .setPolygon(poly) |
| .setLightSourcePosition( |
| (rectScaled.left + rectScaled.right) / 2.0f, |
| (rectScaled.top + rectScaled.bottom) / 2.0f) |
| .setEdgeScale(ShadowConstants.AMBIENT_SHADOW_EDGE_SCALE) |
| .setShadowBoundRatio(ShadowConstants.AMBIENT_SHADOW_SHADOW_BOUND) |
| .setShadowStrength(ShadowConstants.AMBIENT_SHADOW_STRENGTH * alpha) |
| .build(); |
| |
| AmbientShadowTriangulator ambientTriangulator = new AmbientShadowTriangulator(ambientConfig); |
| ambientTriangulator.triangulate(); |
| |
| SpotShadowTriangulator spotTriangulator = null; |
| float lightZHeightPx = ShadowConstants.SPOT_SHADOW_LIGHT_Z_HEIGHT_DP * (densityDpi / DisplayMetrics.DENSITY_DEFAULT); |
| if (lightZHeightPx - elevation / SCALE_DOWN >= ShadowConstants.SPOT_SHADOW_LIGHT_Z_EPSILON) { |
| |
| float lightX = (rectScaled.left + rectScaled.right) / 2; |
| float lightY = rectScaled.top; |
| // Light shouldn't be bigger than the object by too much. |
| int dynamicLightRadius = Math.min(rectScaled.width(), rectScaled.height()); |
| |
| SpotShadowConfig spotConfig = new SpotShadowConfig.Builder() |
| .setLightCoord(lightX, lightY, lightZHeightPx) |
| .setLightRadius(dynamicLightRadius) |
| .setShadowStrength(ShadowConstants.SPOT_SHADOW_STRENGTH * alpha) |
| .setPolygon(poly, poly.length / ShadowConstants.COORDINATE_SIZE) |
| .build(); |
| |
| spotTriangulator = new SpotShadowTriangulator(spotConfig); |
| spotTriangulator.triangulate(); |
| } |
| |
| int translateX = 0; |
| int translateY = 0; |
| int imgW = 0; |
| int imgH = 0; |
| |
| if (ambientTriangulator.isValid()) { |
| float[] shadowBounds = Math3DHelper.flatBound(ambientTriangulator.getVertices(), 2); |
| // Move the shadow to the left top corner to occupy the least possible bitmap |
| |
| translateX = -(int) Math.floor(shadowBounds[0]); |
| translateY = -(int) Math.floor(shadowBounds[1]); |
| |
| // create bitmap of the least possible size that covers the entire shadow |
| imgW = (int) Math.ceil(shadowBounds[2] + translateX); |
| imgH = (int) Math.ceil(shadowBounds[3] + translateY); |
| } |
| |
| if (spotTriangulator != null && spotTriangulator.validate()) { |
| |
| // Bit of a hack to re-adjust spot shadow to fit correctly within parent canvas. |
| // Problem is that outline passed is not a final position, which throws off our |
| // whereas our shadow rendering algorithm, which requires pre-set range for |
| // optimization purposes. |
| float[] shadowBounds = Math3DHelper.flatBound(spotTriangulator.getStrips()[0], 3); |
| |
| if ((shadowBounds[2] - shadowBounds[0]) > width || |
| (shadowBounds[3] - shadowBounds[1]) > height) { |
| // Spot shadow to be casted is larger than the parent canvas, |
| // We'll let ambient shadow do the trick and skip spot shadow here. |
| spotTriangulator = null; |
| } |
| |
| translateX = Math.max(-(int) Math.floor(shadowBounds[0]), translateX); |
| translateY = Math.max(-(int) Math.floor(shadowBounds[1]), translateY); |
| |
| // create bitmap of the least possible size that covers the entire shadow |
| imgW = Math.max((int) Math.ceil(shadowBounds[2] + translateX), imgW); |
| imgH = Math.max((int) Math.ceil(shadowBounds[3] + translateY), imgH); |
| } |
| |
| TriangleBuffer renderer = new TriangleBuffer(); |
| renderer.setSize(imgW, imgH, 0); |
| |
| if (ambientTriangulator.isValid()) { |
| |
| Math3DHelper.translate(ambientTriangulator.getVertices(), translateX, translateY, 2); |
| renderer.drawTriangles(ambientTriangulator.getIndices(), ambientTriangulator.getVertices(), |
| ambientTriangulator.getColors(), ambientConfig.getShadowStrength()); |
| } |
| |
| if (spotTriangulator != null && spotTriangulator.validate()) { |
| float[][] strips = spotTriangulator.getStrips(); |
| for (int i = 0; i < strips.length; ++i) { |
| Math3DHelper.translate(strips[i], translateX, translateY, 3); |
| renderer.drawTriangles(strips[i], ShadowConstants.SPOT_SHADOW_STRENGTH * alpha); |
| } |
| } |
| |
| Bitmap img = renderer.createImage(); |
| |
| drawScaled(canvas, img, translateX, translateY, rectOriginal, radius, isOpaque); |
| } |
| |
| /** |
| * High quality shadow does not work well with object that is too high in elevation. Check if |
| * the object elevation is reasonable and returns true if shadow will work well. False other |
| * wise. |
| */ |
| private static boolean validate(float elevation, float densityDpi) { |
| float scaledElevationPx = elevation / SCALE_DOWN; |
| float scaledSpotLightHeightPx = ShadowConstants.SPOT_SHADOW_LIGHT_Z_HEIGHT_DP * |
| (densityDpi / DisplayMetrics.DENSITY_DEFAULT); |
| if (scaledElevationPx > scaledSpotLightHeightPx) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Draw the bitmap scaled up. |
| * @param translateX - offset in x axis by which the bitmap is shifted. |
| * @param translateY - offset in y axis by which the bitmap is shifted. |
| * @param shadowCaster - unscaled outline of shadow caster |
| * @param radius |
| */ |
| private static void drawScaled(Canvas canvas, Bitmap bitmap, int translateX, int translateY, |
| Rect shadowCaster, float radius, boolean isOpaque) { |
| int unscaledTranslateX = translateX * SCALE_DOWN; |
| int unscaledTranslateY = translateY * SCALE_DOWN; |
| |
| // To the canvas |
| Rect dest = new Rect( |
| -unscaledTranslateX, |
| -unscaledTranslateY, |
| (bitmap.getWidth() * SCALE_DOWN) - unscaledTranslateX, |
| (bitmap.getHeight() * SCALE_DOWN) - unscaledTranslateY); |
| Rect destSrc = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); |
| // We can skip drawing the shadows behind the caster if either |
| // 1) radius is 0, the shadow caster is rectangle and we can have a perfect cut |
| // 2) shadow caster is opaque and even if remove shadow only partially it won't affect |
| // the visual quality, otherwise we will observe shadow part through the translucent caster |
| // This can be improved by: |
| // TODO: do not draw the shadow behind the caster at all during the tesselation phase |
| if (radius > 0 && !isOpaque) { |
| // Rounded edge. |
| int save = canvas.save(); |
| canvas.drawBitmap(bitmap, destSrc, dest, null); |
| canvas.restoreToCount(save); |
| return; |
| } |
| |
| /** |
| * ---------------------------------- |
| * | | |
| * | top | |
| * | | |
| * ---------------------------------- |
| * | | | | |
| * | left | shadow caster | right | |
| * | | | | |
| * ---------------------------------- |
| * | | |
| * | bottom | |
| * | | |
| * ---------------------------------- |
| * |
| * dest == top + left + shadow caster + right + bottom |
| * Visually, canvas.drawBitmap(bitmap, destSrc, dest, paint) would achieve the same result. |
| */ |
| int gap = (int) Math.ceil(radius * SCALE_DOWN * sRoundedGap); |
| shadowCaster.bottom -= gap; |
| shadowCaster.top += gap; |
| shadowCaster.left += gap; |
| shadowCaster.right -= gap; |
| Rect left = new Rect(dest.left, shadowCaster.top, shadowCaster.left, |
| shadowCaster.bottom); |
| int leftScaled = left.width() / SCALE_DOWN + destSrc.left; |
| |
| Rect top = new Rect(dest.left, dest.top, dest.right, shadowCaster.top); |
| int topScaled = top.height() / SCALE_DOWN + destSrc.top; |
| |
| Rect right = new Rect(shadowCaster.right, shadowCaster.top, dest.right, |
| shadowCaster.bottom); |
| int rightScaled = (shadowCaster.right - dest.left) / SCALE_DOWN + destSrc.left; |
| |
| Rect bottom = new Rect(dest.left, shadowCaster.bottom, dest.right, dest.bottom); |
| int bottomScaled = (shadowCaster.bottom - dest.top) / SCALE_DOWN + destSrc.top; |
| |
| // calculate parts of the middle ground that can be ignored. |
| Rect leftSrc = new Rect(destSrc.left, topScaled, leftScaled, bottomScaled); |
| Rect topSrc = new Rect(destSrc.left, destSrc.top, destSrc.right, topScaled); |
| Rect rightSrc = new Rect(rightScaled, topScaled, destSrc.right, bottomScaled); |
| Rect bottomSrc = new Rect(destSrc.left, bottomScaled, destSrc.right, destSrc.bottom); |
| |
| int save = canvas.save(); |
| Paint paint = new Paint(); |
| canvas.drawBitmap(bitmap, leftSrc, left, paint); |
| canvas.drawBitmap(bitmap, topSrc, top, paint); |
| canvas.drawBitmap(bitmap, rightSrc, right, paint); |
| canvas.drawBitmap(bitmap, bottomSrc, bottom, paint); |
| canvas.restoreToCount(save); |
| } |
| |
| private static float[] getPoly(Rect rect, float elevation, float radius) { |
| if (radius <= 0) { |
| float[] poly = new float[ShadowConstants.RECT_VERTICES_SIZE * ShadowConstants.COORDINATE_SIZE]; |
| |
| poly[0] = poly[9] = rect.left; |
| poly[1] = poly[4] = rect.top; |
| poly[3] = poly[6] = rect.right; |
| poly[7] = poly[10] = rect.bottom; |
| poly[2] = poly[5] = poly[8] = poly[11] = elevation; |
| |
| return poly; |
| } |
| |
| return buildRoundedEdges(rect, elevation, radius); |
| } |
| |
| private static float[] buildRoundedEdges( |
| Rect rect, float elevation, float radius) { |
| |
| float[] roundedEdgeVertices = new float[(ShadowConstants.SPLICE_ROUNDED_EDGE + 1) * 4 * 3]; |
| int index = 0; |
| // 1.0 LT. From theta 0 to pi/2 in K division. |
| for (int i = 0; i <= ShadowConstants.SPLICE_ROUNDED_EDGE; i++) { |
| double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE); |
| float x = (float) (rect.left + (radius - radius * Math.cos(theta))); |
| float y = (float) (rect.top + (radius - radius * Math.sin(theta))); |
| roundedEdgeVertices[index++] = x; |
| roundedEdgeVertices[index++] = y; |
| roundedEdgeVertices[index++] = elevation; |
| } |
| |
| // 2.0 RT |
| for (int i = ShadowConstants.SPLICE_ROUNDED_EDGE; i >= 0; i--) { |
| double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE); |
| float x = (float) (rect.right - (radius - radius * Math.cos(theta))); |
| float y = (float) (rect.top + (radius - radius * Math.sin(theta))); |
| roundedEdgeVertices[index++] = x; |
| roundedEdgeVertices[index++] = y; |
| roundedEdgeVertices[index++] = elevation; |
| } |
| |
| // 3.0 RB |
| for (int i = 0; i <= ShadowConstants.SPLICE_ROUNDED_EDGE; i++) { |
| double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE); |
| float x = (float) (rect.right - (radius - radius * Math.cos(theta))); |
| float y = (float) (rect.bottom - (radius - radius * Math.sin(theta))); |
| roundedEdgeVertices[index++] = x; |
| roundedEdgeVertices[index++] = y; |
| roundedEdgeVertices[index++] = elevation; |
| } |
| |
| // 4.0 LB |
| for (int i = ShadowConstants.SPLICE_ROUNDED_EDGE; i >= 0; i--) { |
| double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE); |
| float x = (float) (rect.left + (radius - radius * Math.cos(theta))); |
| float y = (float) (rect.bottom - (radius - radius * Math.sin(theta))); |
| roundedEdgeVertices[index++] = x; |
| roundedEdgeVertices[index++] = y; |
| roundedEdgeVertices[index++] = elevation; |
| } |
| |
| return roundedEdgeVertices; |
| } |
| } |