Add primitive shadows support to LayoutLib

The shadows rendered are not the same as rendered by the framework. The
main purpose of the shadows here is let the user know when shadows will
be cast.

Currently rectangular shadows are available in only two intensities. Any
elevation below 10dp uses one shadow and any elevation over 10 dp uses
the other.

Also, even though the code for arbitrary shapes exist, it doesn't quite
work yet. No shadows are visible when that code is run.

Change-Id: Ie8235ffccf1d3809713f5d8f82afde434817d6b2
This commit is contained in:
Deepanshu Gupta
2014-11-04 18:19:55 -08:00
parent 9173c8a2e9
commit 796e9b7f99
20 changed files with 798 additions and 59 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2014 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;
import com.android.layoutlib.bridge.impl.DelegateManager;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
/**
* Delegate implementing the native methods of {@link RenderNode}
* <p/>
* Through the layoutlib_create tool, some native methods of RenderNode have been replaced by calls
* to methods of the same name in this delegate class.
*
* @see DelegateManager
*/
public class RenderNode_Delegate {
// ---- delegate manager ----
private static final DelegateManager<RenderNode_Delegate> sManager =
new DelegateManager<RenderNode_Delegate>(RenderNode_Delegate.class);
private float mLift;
@SuppressWarnings("UnusedDeclaration")
private String mName;
@LayoutlibDelegate
/*package*/ static long nCreate(String name) {
RenderNode_Delegate renderNodeDelegate = new RenderNode_Delegate();
renderNodeDelegate.mName = name;
return sManager.addNewDelegate(renderNodeDelegate);
}
@LayoutlibDelegate
/*package*/ static void nDestroyRenderNode(long renderNode) {
sManager.removeJavaReferenceFor(renderNode);
}
@LayoutlibDelegate
/*package*/ static boolean nSetElevation(long renderNode, float lift) {
RenderNode_Delegate delegate = sManager.getDelegate(renderNode);
if (delegate != null && delegate.mLift != lift) {
delegate.mLift = lift;
return true;
}
return false;
}
@LayoutlibDelegate
/*package*/ static float nGetElevation(long renderNode) {
RenderNode_Delegate delegate = sManager.getDelegate(renderNode);
if (delegate != null) {
return delegate.mLift;
}
return 0f;
}
}

View File

@@ -0,0 +1,415 @@
/*
* Copyright (C) 2014 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;
import com.android.annotations.NonNull;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.IOException;
import java.io.InputStream;
import javax.imageio.ImageIO;
public class ShadowPainter {
/**
* Adds a drop shadow to a semi-transparent image (of an arbitrary shape) and returns it as a
* new image. This method attempts to mimic the same visual characteristics as the rectangular
* shadow painting methods in this class, {@link #createRectangularDropShadow(java.awt.image.BufferedImage)}
* and {@link #createSmallRectangularDropShadow(java.awt.image.BufferedImage)}.
*
* @param source the source image
* @param shadowSize the size of the shadow, normally {@link #SHADOW_SIZE or {@link
* #SMALL_SHADOW_SIZE}}
*
* @return a new image with the shadow painted in
*/
@NonNull
public static BufferedImage createDropShadow(BufferedImage source, int shadowSize) {
shadowSize /= 2; // make shadow size have the same meaning as in the other shadow paint methods in this class
return createDropShadow(source, shadowSize, 0.7f, 0);
}
/**
* Creates a drop shadow of a given image and returns a new image which shows the input image on
* top of its drop shadow.
* <p/>
* <b>NOTE: If the shape is rectangular and opaque, consider using {@link
* #drawRectangleShadow(Graphics2D, int, int, int, int)} instead.</b>
*
* @param source the source image to be shadowed
* @param shadowSize the size of the shadow in pixels
* @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque
* @param shadowRgb the RGB int to use for the shadow color
*
* @return a new image with the source image on top of its shadow
*/
@SuppressWarnings({"SuspiciousNameCombination", "UnnecessaryLocalVariable"}) // Imported code
public static BufferedImage createDropShadow(BufferedImage source, int shadowSize,
float shadowOpacity, int shadowRgb) {
// This code is based on
// http://www.jroller.com/gfx/entry/non_rectangular_shadow
BufferedImage image;
int width = source.getWidth();
int height = source.getHeight();
image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = image.createGraphics();
g2.drawImage(image, shadowSize, shadowSize, null);
int dstWidth = image.getWidth();
int dstHeight = image.getHeight();
int left = (shadowSize - 1) >> 1;
int right = shadowSize - left;
int xStart = left;
int xStop = dstWidth - right;
int yStart = left;
int yStop = dstHeight - right;
shadowRgb &= 0x00FFFFFF;
int[] aHistory = new int[shadowSize];
int historyIdx;
int aSum;
int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
int lastPixelOffset = right * dstWidth;
float sumDivider = shadowOpacity / shadowSize;
// horizontal pass
for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) {
aSum = 0;
historyIdx = 0;
for (int x = 0; x < shadowSize; x++, bufferOffset++) {
int a = dataBuffer[bufferOffset] >>> 24;
aHistory[x] = a;
aSum += a;
}
bufferOffset -= right;
for (int x = xStart; x < xStop; x++, bufferOffset++) {
int a = (int) (aSum * sumDivider);
dataBuffer[bufferOffset] = a << 24 | shadowRgb;
// subtract the oldest pixel from the sum
aSum -= aHistory[historyIdx];
// get the latest pixel
a = dataBuffer[bufferOffset + right] >>> 24;
aHistory[historyIdx] = a;
aSum += a;
if (++historyIdx >= shadowSize) {
historyIdx -= shadowSize;
}
}
}
// vertical pass
for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) {
aSum = 0;
historyIdx = 0;
for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) {
int a = dataBuffer[bufferOffset] >>> 24;
aHistory[y] = a;
aSum += a;
}
bufferOffset -= lastPixelOffset;
for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) {
int a = (int) (aSum * sumDivider);
dataBuffer[bufferOffset] = a << 24 | shadowRgb;
// subtract the oldest pixel from the sum
aSum -= aHistory[historyIdx];
// get the latest pixel
a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24;
aHistory[historyIdx] = a;
aSum += a;
if (++historyIdx >= shadowSize) {
historyIdx -= shadowSize;
}
}
}
g2.drawImage(source, null, 0, 0);
g2.dispose();
return image;
}
/**
* Draws a rectangular drop shadow (of size {@link #SHADOW_SIZE} by {@link #SHADOW_SIZE} around
* the given source and returns a new image with both combined
*
* @param source the source image
*
* @return the source image with a drop shadow on the bottom and right
*/
@SuppressWarnings("UnusedDeclaration")
public static BufferedImage createRectangularDropShadow(BufferedImage source) {
int type = source.getType();
if (type == BufferedImage.TYPE_CUSTOM) {
type = BufferedImage.TYPE_INT_ARGB;
}
int width = source.getWidth();
int height = source.getHeight();
BufferedImage image;
image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE, type);
Graphics2D g = image.createGraphics();
g.drawImage(source, 0, 0, null);
drawRectangleShadow(image, 0, 0, width, height);
g.dispose();
return image;
}
/**
* Draws a small rectangular drop shadow (of size {@link #SMALL_SHADOW_SIZE} by {@link
* #SMALL_SHADOW_SIZE} around the given source and returns a new image with both combined
*
* @param source the source image
*
* @return the source image with a drop shadow on the bottom and right
*/
@SuppressWarnings("UnusedDeclaration")
public static BufferedImage createSmallRectangularDropShadow(BufferedImage source) {
int type = source.getType();
if (type == BufferedImage.TYPE_CUSTOM) {
type = BufferedImage.TYPE_INT_ARGB;
}
int width = source.getWidth();
int height = source.getHeight();
BufferedImage image;
image = new BufferedImage(width + SMALL_SHADOW_SIZE, height + SMALL_SHADOW_SIZE, type);
Graphics2D g = image.createGraphics();
g.drawImage(source, 0, 0, null);
drawSmallRectangleShadow(image, 0, 0, width, height);
g.dispose();
return image;
}
/**
* Draws a drop shadow for the given rectangle into the given context. It will not draw anything
* if the rectangle is smaller than a minimum determined by the assets used to draw the shadow
* graphics. The size of the shadow is {@link #SHADOW_SIZE}.
*
* @param image the image to draw the shadow into
* @param x the left coordinate of the left hand side of the rectangle
* @param y the top coordinate of the top of the rectangle
* @param width the width of the rectangle
* @param height the height of the rectangle
*/
public static void drawRectangleShadow(BufferedImage image,
int x, int y, int width, int height) {
Graphics2D gc = image.createGraphics();
try {
drawRectangleShadow(gc, x, y, width, height);
} finally {
gc.dispose();
}
}
/**
* Draws a small drop shadow for the given rectangle into the given context. It will not draw
* anything if the rectangle is smaller than a minimum determined by the assets used to draw the
* shadow graphics. The size of the shadow is {@link #SMALL_SHADOW_SIZE}.
*
* @param image the image to draw the shadow into
* @param x the left coordinate of the left hand side of the rectangle
* @param y the top coordinate of the top of the rectangle
* @param width the width of the rectangle
* @param height the height of the rectangle
*/
public static void drawSmallRectangleShadow(BufferedImage image,
int x, int y, int width, int height) {
Graphics2D gc = image.createGraphics();
try {
drawSmallRectangleShadow(gc, x, y, width, height);
} finally {
gc.dispose();
}
}
/**
* The width and height of the drop shadow painted by
* {@link #drawRectangleShadow(Graphics2D, int, int, int, int)}
*/
public static final int SHADOW_SIZE = 20; // DO NOT EDIT. This corresponds to bitmap graphics
/**
* The width and height of the drop shadow painted by
* {@link #drawSmallRectangleShadow(Graphics2D, int, int, int, int)}
*/
public static final int SMALL_SHADOW_SIZE = 10; // DO NOT EDIT. Corresponds to bitmap graphics
/**
* Draws a drop shadow for the given rectangle into the given context. It will not draw anything
* if the rectangle is smaller than a minimum determined by the assets used to draw the shadow
* graphics.
*
* @param gc the graphics context to draw into
* @param x the left coordinate of the left hand side of the rectangle
* @param y the top coordinate of the top of the rectangle
* @param width the width of the rectangle
* @param height the height of the rectangle
*/
public static void drawRectangleShadow(Graphics2D gc, int x, int y, int width, int height) {
assert ShadowBottomLeft != null;
assert ShadowBottomRight.getWidth(null) == SHADOW_SIZE;
assert ShadowBottomRight.getHeight(null) == SHADOW_SIZE;
int blWidth = ShadowBottomLeft.getWidth(null);
int trHeight = ShadowTopRight.getHeight(null);
if (width < blWidth) {
return;
}
if (height < trHeight) {
return;
}
gc.drawImage(ShadowBottomLeft, x - ShadowBottomLeft.getWidth(null), y + height, null);
gc.drawImage(ShadowBottomRight, x + width, y + height, null);
gc.drawImage(ShadowTopRight, x + width, y, null);
gc.drawImage(ShadowTopLeft, x - ShadowTopLeft.getWidth(null), y, null);
gc.drawImage(ShadowBottom,
x, y + height, x + width, y + height + ShadowBottom.getHeight(null),
0, 0, ShadowBottom.getWidth(null), ShadowBottom.getHeight(null), null);
gc.drawImage(ShadowRight,
x + width, y + ShadowTopRight.getHeight(null), x + width + ShadowRight.getWidth(null), y + height,
0, 0, ShadowRight.getWidth(null), ShadowRight.getHeight(null), null);
gc.drawImage(ShadowLeft,
x - ShadowLeft.getWidth(null), y + ShadowTopLeft.getHeight(null), x, y + height,
0, 0, ShadowLeft.getWidth(null), ShadowLeft.getHeight(null), null);
}
/**
* Draws a small drop shadow for the given rectangle into the given context. It will not draw
* anything if the rectangle is smaller than a minimum determined by the assets used to draw the
* shadow graphics.
* <p/>
*
* @param gc the graphics context to draw into
* @param x the left coordinate of the left hand side of the rectangle
* @param y the top coordinate of the top of the rectangle
* @param width the width of the rectangle
* @param height the height of the rectangle
*/
public static void drawSmallRectangleShadow(Graphics2D gc, int x, int y, int width,
int height) {
assert Shadow2BottomLeft != null;
assert Shadow2TopRight != null;
assert Shadow2BottomRight.getWidth(null) == SMALL_SHADOW_SIZE;
assert Shadow2BottomRight.getHeight(null) == SMALL_SHADOW_SIZE;
int blWidth = Shadow2BottomLeft.getWidth(null);
int trHeight = Shadow2TopRight.getHeight(null);
if (width < blWidth) {
return;
}
if (height < trHeight) {
return;
}
gc.drawImage(Shadow2BottomLeft, x - Shadow2BottomLeft.getWidth(null), y + height, null);
gc.drawImage(Shadow2BottomRight, x + width, y + height, null);
gc.drawImage(Shadow2TopRight, x + width, y, null);
gc.drawImage(Shadow2TopLeft, x - Shadow2TopLeft.getWidth(null), y, null);
gc.drawImage(Shadow2Bottom,
x, y + height, x + width, y + height + Shadow2Bottom.getHeight(null),
0, 0, Shadow2Bottom.getWidth(null), Shadow2Bottom.getHeight(null), null);
gc.drawImage(Shadow2Right,
x + width, y + Shadow2TopRight.getHeight(null), x + width + Shadow2Right.getWidth(null), y + height,
0, 0, Shadow2Right.getWidth(null), Shadow2Right.getHeight(null), null);
gc.drawImage(Shadow2Left,
x - Shadow2Left.getWidth(null), y + Shadow2TopLeft.getHeight(null), x, y + height,
0, 0, Shadow2Left.getWidth(null), Shadow2Left.getHeight(null), null);
}
private static Image loadIcon(String name) {
InputStream inputStream = ShadowPainter.class.getResourceAsStream(name);
if (inputStream == null) {
throw new RuntimeException("Unable to load image for shadow: " + name);
}
try {
return ImageIO.read(inputStream);
} catch (IOException e) {
throw new RuntimeException("Unable to load image for shadow:" + name, e);
} finally {
try {
inputStream.close();
} catch (IOException e) {
// ignore.
}
}
}
// Shadow graphics. This was generated by creating a drop shadow in
// Gimp, using the parameters x offset=10, y offset=10, blur radius=10,
// (for the small drop shadows x offset=10, y offset=10, blur radius=10)
// color=black, and opacity=51. These values attempt to make a shadow
// that is legible both for dark and light themes, on top of the
// canvas background (rgb(150,150,150). Darker shadows would tend to
// blend into the foreground for a dark holo screen, and lighter shadows
// would be hard to spot on the canvas background. If you make adjustments,
// make sure to check the shadow with both dark and light themes.
//
// After making the graphics, I cut out the top right, bottom left
// and bottom right corners as 20x20 images, and these are reproduced by
// painting them in the corresponding places in the target graphics context.
// I then grabbed a single horizontal gradient line from the middle of the
// right edge,and a single vertical gradient line from the bottom. These
// are then painted scaled/stretched in the target to fill the gaps between
// the three corner images.
//
// Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right
// Normal Drop Shadow
private static final Image ShadowBottom = loadIcon("/icons/shadow-b.png");
private static final Image ShadowBottomLeft = loadIcon("/icons/shadow-bl.png");
private static final Image ShadowBottomRight = loadIcon("/icons/shadow-br.png");
private static final Image ShadowRight = loadIcon("/icons/shadow-r.png");
private static final Image ShadowTopRight = loadIcon("/icons/shadow-tr.png");
private static final Image ShadowTopLeft = loadIcon("/icons/shadow-tl.png");
private static final Image ShadowLeft = loadIcon("/icons/shadow-l.png");
// Small Drop Shadow
private static final Image Shadow2Bottom = loadIcon("/icons/shadow2-b.png");
private static final Image Shadow2BottomLeft = loadIcon("/icons/shadow2-bl.png");
private static final Image Shadow2BottomRight = loadIcon("/icons/shadow2-br.png");
private static final Image Shadow2Right = loadIcon("/icons/shadow2-r.png");
private static final Image Shadow2TopRight = loadIcon("/icons/shadow2-tr.png");
private static final Image Shadow2TopLeft = loadIcon("/icons/shadow2-tl.png");
private static final Image Shadow2Left = loadIcon("/icons/shadow2-l.png");
}

View File

@@ -0,0 +1,205 @@
/*
* Copyright (C) 2014 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;
import com.android.annotations.NonNull;
import com.android.layoutlib.bridge.android.BridgeContext;
import com.android.resources.Density;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap_Delegate;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Path_Delegate;
import android.graphics.Rect;
import android.graphics.Region.Op;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.animation.Transformation;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
/**
* Delegate used to provide new implementation of a select few methods of {@link ViewGroup}
* <p/>
* Through the layoutlib_create tool, the original methods of ViewGroup have been replaced by calls
* to methods of the same name in this delegate class.
*/
public class ViewGroup_Delegate {
/**
* Overrides the original drawChild call in ViewGroup to draw the shadow.
*/
@LayoutlibDelegate
/*package*/ static boolean drawChild(ViewGroup thisVG, Canvas canvas, View child,
long drawingTime) {
boolean retVal = thisVG.drawChild_Original(canvas, child, drawingTime);
if (child.getZ() > thisVG.getZ()) {
ViewOutlineProvider outlineProvider = child.getOutlineProvider();
Outline outline = new Outline();
outlineProvider.getOutline(child, outline);
if (outline.mPath != null || (outline.mRect != null && !outline.mRect.isEmpty())) {
int restoreTo = transformCanvas(thisVG, canvas, child);
drawShadow(thisVG, canvas, child, outline);
canvas.restoreToCount(restoreTo);
}
}
return retVal;
}
private static void drawShadow(ViewGroup parent, Canvas canvas, View child,
Outline outline) {
BufferedImage shadow = null;
int x = 0;
if (outline.mRect != null) {
Shadow s = getRectShadow(parent, canvas, child, outline);
shadow = s.mShadow;
x = -s.mShadowWidth;
} else if (outline.mPath != null) {
shadow = getPathShadow(child, outline, canvas);
}
if (shadow == null) {
return;
}
Bitmap bitmap = Bitmap_Delegate.createBitmap(shadow, false,
Density.getEnum(canvas.getDensity()));
Rect clipBounds = canvas.getClipBounds();
Rect newBounds = new Rect(clipBounds);
newBounds.left = newBounds.left + x;
canvas.clipRect(newBounds, Op.REPLACE);
canvas.drawBitmap(bitmap, x, 0, null);
canvas.clipRect(clipBounds, Op.REPLACE);
}
private static Shadow getRectShadow(ViewGroup parent, Canvas canvas, View child,
Outline outline) {
BufferedImage shadow;
Rect clipBounds = canvas.getClipBounds();
if (clipBounds.isEmpty()) {
return null;
}
float height = child.getZ() - parent.getZ();
// Draw large shadow if difference in z index is more than 10dp
float largeShadowThreshold = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f,
getMetrics(child));
boolean largeShadow = height > largeShadowThreshold;
int shadowSize = largeShadow ? ShadowPainter.SHADOW_SIZE : ShadowPainter.SMALL_SHADOW_SIZE;
shadow = new BufferedImage(clipBounds.width() + shadowSize, clipBounds.height(),
BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = shadow.createGraphics();
Rect rect = outline.mRect;
if (largeShadow) {
ShadowPainter.drawRectangleShadow(graphics,
rect.left + shadowSize, rect.top, rect.width(), rect.height());
} else {
ShadowPainter.drawSmallRectangleShadow(graphics,
rect.left + shadowSize, rect.top, rect.width(), rect.height());
}
graphics.dispose();
return new Shadow(shadow, shadowSize);
}
@NonNull
private static DisplayMetrics getMetrics(View view) {
Context context = view.getContext();
while (context instanceof ContextThemeWrapper) {
context = ((ContextThemeWrapper) context).getBaseContext();
}
if (context instanceof BridgeContext) {
return ((BridgeContext) context).getMetrics();
}
throw new RuntimeException("View " + view.getClass().getName() + " not created with the " +
"right context");
}
private static BufferedImage getPathShadow(View child, Outline outline, Canvas canvas) {
Rect clipBounds = canvas.getClipBounds();
BufferedImage image = new BufferedImage(clipBounds.width(), clipBounds.height(),
BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
graphics.draw(Path_Delegate.getDelegate(outline.mPath.mNativePath).getJavaShape());
graphics.dispose();
return ShadowPainter.createDropShadow(image, ((int) child.getZ()));
}
// Copied from android.view.View#draw(Canvas, ViewGroup, long) and removed code paths
// which were never taken. Ideally, we should hook up the shadow code in the same method so
// that we don't have to transform the canvas twice.
private static int transformCanvas(ViewGroup thisVG, Canvas canvas, View child) {
final int restoreTo = canvas.save();
final boolean childHasIdentityMatrix = child.hasIdentityMatrix();
int flags = thisVG.mGroupFlags;
Transformation transformToApply = null;
boolean concatMatrix = false;
if ((flags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
final Transformation t = thisVG.getChildTransformation();
final boolean hasTransform = thisVG.getChildStaticTransformation(child, t);
if (hasTransform) {
final int transformType = t.getTransformationType();
transformToApply = transformType != Transformation.TYPE_IDENTITY ? t : null;
concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
}
}
concatMatrix |= childHasIdentityMatrix;
child.computeScroll();
int sx = child.mScrollX;
int sy = child.mScrollY;
canvas.translate(child.mLeft - sx, child.mTop - sy);
float alpha = child.getAlpha() * child.getTransitionAlpha();
if (transformToApply != null || alpha < 1 || !childHasIdentityMatrix) {
if (transformToApply != null || !childHasIdentityMatrix) {
int transX = -sx;
int transY = -sy;
if (transformToApply != null) {
if (concatMatrix) {
// Undo the scroll translation, apply the transformation matrix,
// then redo the scroll translate to get the correct result.
canvas.translate(-transX, -transY);
canvas.concat(transformToApply.getMatrix());
canvas.translate(transX, transY);
}
if (!childHasIdentityMatrix) {
canvas.translate(-transX, -transY);
canvas.concat(child.getMatrix());
canvas.translate(transX, transY);
}
}
}
}
return restoreTo;
}
private static class Shadow {
public BufferedImage mShadow;
public int mShadowWidth;
public Shadow(BufferedImage shadow, int shadowWidth) {
mShadow = shadow;
mShadowWidth = shadowWidth;
}
}
}

View File

@@ -18,6 +18,7 @@ package com.android.layoutlib.bridge.bars;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.RenderResources;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.SessionParams;
import com.android.internal.R;
@@ -37,7 +38,6 @@ import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ActionMenuPresenter;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.RelativeLayout;
@@ -49,15 +49,23 @@ public class ActionBarLayout {
private static final String LAYOUT_ATTR_NAME = "windowActionBarFullscreenDecorLayout";
// The Action Bar
@NonNull private CustomActionBarWrapper mActionBar;
@NonNull
private CustomActionBarWrapper mActionBar;
// Store another reference to the context so that we don't have to cast it repeatedly.
@NonNull private final BridgeContext mBridgeContext;
@NonNull
private final BridgeContext mBridgeContext;
@NonNull private FrameLayout mContentRoot;
@NonNull
private FrameLayout mContentRoot;
// A fake parent for measuring views.
@Nullable private ViewGroup mMeasureParent;
@Nullable
private ViewGroup mMeasureParent;
// A Layout that contains the inflated action bar. The menu popup is added to this layout.
@NonNull
private final RelativeLayout mEnclosingLayout;
/**
* Inflate the action bar and attach it to {@code parentView}
@@ -90,20 +98,25 @@ public class ActionBarLayout {
if (layoutId == 0) {
throw new RuntimeException(error);
}
// Create a RelativeLayout to hold the action bar. The layout is needed so that we may
// add the menu popup to it.
mEnclosingLayout = new RelativeLayout(mBridgeContext);
setMatchParent(mEnclosingLayout);
parentView.addView(mEnclosingLayout);
// Inflate action bar layout.
View decorContent = LayoutInflater.from(context).inflate(layoutId, parentView, true);
View decorContent = LayoutInflater.from(context).inflate(layoutId, mEnclosingLayout, true);
mActionBar = CustomActionBarWrapper.getActionBarWrapper(context, params, decorContent);
FrameLayout contentRoot = (FrameLayout) parentView.findViewById(android.R.id.content);
FrameLayout contentRoot = (FrameLayout) mEnclosingLayout.findViewById(android.R.id.content);
// If something went wrong and we were not able to initialize the content root,
// just add a frame layout inside this and return.
if (contentRoot == null) {
contentRoot = new FrameLayout(context);
contentRoot.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
parentView.addView(contentRoot);
setMatchParent(contentRoot);
mEnclosingLayout.addView(contentRoot);
mContentRoot = contentRoot;
} else {
mContentRoot = contentRoot;
@@ -112,70 +125,49 @@ public class ActionBarLayout {
}
}
private void setMatchParent(View view) {
view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
}
/**
* Creates a Popup and adds it to the content frame. It also adds another {@link FrameLayout} to
* the content frame which shall serve as the new content root.
*/
public void createMenuPopup() {
assert mContentRoot.getId() == android.R.id.content
assert mEnclosingLayout.getChildCount() == 1
: "Action Bar Menus have already been created.";
if (!isOverflowPopupNeeded()) {
return;
}
// Create a layout to hold the menus and the user's content.
RelativeLayout layout = new RelativeLayout(mActionBar.getPopupContext());
layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
mContentRoot.addView(layout);
// Create a layout for the user's content.
FrameLayout contentRoot = new FrameLayout(mBridgeContext);
contentRoot.setLayoutParams(new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
// Add contentRoot and menus to the layout.
layout.addView(contentRoot);
layout.addView(createMenuView());
// ContentRoot is now the view we just created.
mContentRoot = contentRoot;
}
/**
* Returns a {@link LinearLayout} containing the menu list view to be embedded in a
* {@link RelativeLayout}
*/
@NonNull
private View createMenuView() {
DisplayMetrics metrics = mBridgeContext.getMetrics();
MenuBuilder menu = mActionBar.getMenuBuilder();
OverflowMenuAdapter adapter = new OverflowMenuAdapter(menu, mActionBar.getPopupContext());
LinearLayout layout = new LinearLayout(mActionBar.getPopupContext());
ListView listView = new ListView(mActionBar.getPopupContext(), null,
R.attr.dropDownListViewStyle);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
measureContentWidth(adapter), LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_END);
if (mActionBar.isSplit()) {
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
// TODO: Find correct value instead of hardcoded 10dp.
layoutParams.bottomMargin = getPixelValue("-10dp", metrics);
layoutParams.bottomMargin = getActionBarHeight() + mActionBar.getMenuPopupMargin();
} else {
layoutParams.topMargin = getPixelValue("-10dp", metrics);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
layoutParams.topMargin = getActionBarHeight() + mActionBar.getMenuPopupMargin();
}
layout.setLayoutParams(layoutParams);
layoutParams.setMarginEnd(getPixelValue("5dp", metrics));
listView.setLayoutParams(layoutParams);
listView.setAdapter(adapter);
final TypedArray a = mActionBar.getPopupContext().obtainStyledAttributes(null,
R.styleable.PopupWindow, R.attr.popupMenuStyle, 0);
layout.setBackground(a.getDrawable(R.styleable.PopupWindow_popupBackground));
layout.setDividerDrawable(a.getDrawable(R.attr.actionBarDivider));
listView.setBackground(a.getDrawable(R.styleable.PopupWindow_popupBackground));
listView.setDivider(a.getDrawable(R.attr.actionBarDivider));
a.recycle();
layout.setOrientation(LinearLayout.VERTICAL);
layout.setDividerPadding(getPixelValue("12dp", metrics));
layout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
ListView listView = new ListView(mActionBar.getPopupContext(), null,
R.attr.dropDownListViewStyle);
listView.setAdapter(adapter);
layout.addView(listView);
return layout;
listView.setElevation(mActionBar.getMenuPopupElevation());
mEnclosingLayout.addView(listView);
}
private boolean isOverflowPopupNeeded() {
@@ -244,9 +236,30 @@ public class ActionBarLayout {
return maxWidth;
}
private int getPixelValue(@NonNull String value, @NonNull DisplayMetrics metrics) {
static int getPixelValue(@NonNull String value, @NonNull DisplayMetrics metrics) {
TypedValue typedValue = ResourceHelper.getValue(null, value, false /*requireUnit*/);
return (int) typedValue.getDimension(metrics);
}
// TODO: This is duplicated from RenderSessionImpl.
private int getActionBarHeight() {
RenderResources resources = mBridgeContext.getRenderResources();
DisplayMetrics metrics = mBridgeContext.getMetrics();
ResourceValue value = resources.findItemInTheme("actionBarSize", true);
// resolve it
value = resources.resolveResValue(value);
if (value != null) {
// get the numerical value, if available
TypedValue typedValue = ResourceHelper.getValue("actionBarSize", value.getValue(),
true);
if (typedValue != null) {
// compute the pixel value based on the display metrics
return (int) typedValue.getDimension(metrics);
}
}
return 0;
}
}

View File

@@ -65,18 +65,17 @@ public abstract class CustomActionBarWrapper {
* Returns a wrapper around different implementations of the Action Bar to provide a common API.
*
* @param decorContent the top level view returned by inflating
* ?attr/windowActionBarFullscreenDecorLayout
* ?attr/windowActionBarFullscreenDecorLayout
*/
@NonNull
public static CustomActionBarWrapper getActionBarWrapper(@NonNull BridgeContext context,
@NonNull SessionParams params, @NonNull View decorContent) {
View view = decorContent.findViewById(R.id.action_bar);
if (view instanceof Toolbar) {
return new ToolbarWrapper(context, params, ((Toolbar) view)
);
return new ToolbarWrapper(context, params, ((Toolbar) view));
} else if (view instanceof ActionBarView) {
return new WindowActionBarWrapper(context, params, decorContent, ((ActionBarView) view)
);
return new WindowActionBarWrapper(context, params, decorContent,
((ActionBarView) view));
} else {
throw new IllegalStateException("Can't make an action bar out of " +
view.getClass().getSimpleName());
@@ -174,6 +173,13 @@ public abstract class CustomActionBarWrapper {
@NonNull
abstract DecorToolbar getDecorToolbar();
abstract int getMenuPopupElevation();
/**
* Margin between the menu popup and the action bar.
*/
abstract int getMenuPopupMargin();
// ---- The implementations ----
/**
@@ -226,25 +232,38 @@ public abstract class CustomActionBarWrapper {
DecorToolbar getDecorToolbar() {
return mToolbar.getWrapper();
}
@Override
int getMenuPopupElevation() {
return 10;
}
@Override
int getMenuPopupMargin() {
return 0;
}
}
/**
* Holo theme uses {@link WindowDecorActionBar} as the action bar. This wrapper provides
* access to it using a common API.
*/
private static class WindowActionBarWrapper extends CustomActionBarWrapper{
private static class WindowActionBarWrapper extends CustomActionBarWrapper {
@NonNull
private final WindowDecorActionBar mActionBar;
@NonNull
private final ActionBarView mActionBarView;
@NonNull
private final View mDecorContentRoot;
private MenuBuilder mMenuBuilder;
public WindowActionBarWrapper(@NonNull BridgeContext context, @NonNull SessionParams params,
@NonNull View decorContentRoot, @NonNull ActionBarView actionBarView) {
super(context, params, new WindowDecorActionBar(decorContentRoot)
);
super(context, params, new WindowDecorActionBar(decorContentRoot));
mActionBarView = actionBarView;
mActionBar = ((WindowDecorActionBar) super.mActionBar);
mDecorContentRoot = decorContentRoot;
}
@Override
@@ -270,7 +289,7 @@ public abstract class CustomActionBarWrapper {
}
// Set action bar to be split, if needed.
ViewGroup splitView = (ViewGroup) mActionBarView.findViewById(R.id.split_action_bar);
ViewGroup splitView = (ViewGroup) mDecorContentRoot.findViewById(R.id.split_action_bar);
if (splitView != null) {
mActionBarView.setSplitView(splitView);
Resources res = mContext.getResources();
@@ -314,6 +333,16 @@ public abstract class CustomActionBarWrapper {
return mActionBarView;
}
@Override
int getMenuPopupElevation() {
return 0;
}
@Override
int getMenuPopupMargin() {
return -ActionBarLayout.getPixelValue("10dp", mContext.getMetrics());
}
// TODO: Use an adapter, like List View to set up tabs.
@SuppressWarnings("deprecation") // For Tab
private void setupTabs(int num) {

View File

@@ -162,6 +162,11 @@ public final class CreateInfo implements ICreateInfo {
"android.view.WindowManagerGlobal#getWindowManagerService",
"android.view.inputmethod.InputMethodManager#getInstance",
"android.view.MenuInflater#registerMenu",
"android.view.RenderNode#nCreate",
"android.view.RenderNode#nDestroyRenderNode",
"android.view.RenderNode#nSetElevation",
"android.view.RenderNode#nGetElevation",
"android.view.ViewGroup#drawChild",
"com.android.internal.view.menu.MenuBuilder#createNewMenuItem",
"com.android.internal.util.XmlUtils#convertValueToInt",
"com.android.internal.textservice.ITextServicesManager$Stub#asInterface",