Expanding a ViewGroup's surface to include text shadows - android

In an Android project, I am using FrameLayout to contain a TextView. This TextView has a large shadow on it (to provide a glow). However, Android is detecting the TextView's bounding rect to be just that of the contained text, without the shadow, and as a result the compositor gets confused when objects animate around it.
As a workaround I tried forcing the TextView to have its own hardware surface (with setLayerType), but this too is closely cropped to the text and doesn't account for the shadow, so the shadow gets cut off.
I have tried adding padding to the FrameLayout, but that doesn't expand the surface - it only moves the TextView down and right by the padding amount.
If I set a background color on the FrameLayout, the surface does expand to cover its entire size, but unfortunately this background is visible, even if I set it to 0x01000000. If there were some way to force it to contain the entire background even if the background color is 0, that would be a suitable solution.
What is the easiest way to expand the hardware surface to include the text glow, ideally without affecting the position of the text itself?

The trick is to get Canvas to believe that every pixel has been touched, without actually having anything to paint. Simply drawing a ColorDrawable does not work, because every step along the way seems to detect that a fully-transparent color doesn't need to be rendered. And, apparently the render for the text shadow is also only rendering where the canvas thinks it's been invalidated – so, for example, only drawing a couple of background pixels will expand the surface but still does not cause the actual text shadow to be rendered.
However, there is an operation that will cause Canvas's clipping mask to get updated even with transparent pixels – a transparent Bitmap!
So, adding a background Drawable that renders out a transparent Bitmap has the desired effect. You can add a transparent bitmap as a resource (drawn using a BitmapDrawable), or you can do this:
final Bitmap bmp = Bitmap.createBitmap(2, 2, Bitmap.Config.ALPHA_8);
final Paint bmpPaint = new Paint();
wrappingFrameLayout.setBackground(new Drawable() {
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
public void draw(final Canvas c) {
c.drawBitmap(bmp, null, getBounds(), bmpPaint);
}
public void setAlpha(final int a) {}
public void setColorFilter(final ColorFilter cf) {}
});

Related

Drawing outside canvas on android drawable

I am trying to draw shadow on my drawable, but the shadow gets cropped, since the rectangle I am drawing is the size of canvas.getClipBounds().
In order to draw the shadow I set the hardware acceleration on the view
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
And I also set the Shadow Layer in my Custom Drawable
bgPaint.setShadowLayer(radius, 0, 0, Color.Black);
When I draw a small rectangle the shadow is rendered. So, I know my original problem is about the shadowing being discarded for being outside of the canvas.
I tried some approaches, such as:
Setting the Drawable's bounds,
Increasing the canvas' bounds,
Disable clipChildren on the parent view,
Overriding Drawable.getPadding()
But none of them helped me.

Android View outline - using a custom Path

When I set View's outline as follows it works just fine:
view.setClipToOutline(true);
view.setOutlineProvider(new ViewOutlineProvider() {
#Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), 16);
}
});
However, using a Path has no effect. Replace setRoundRect with:
Path path = new Path();
path.addRoundRect(
0, 0, view.getWidth(), view.getHeight(), 16, 16, Path.Direction.CW);
outline.setConvexPath(path);
How to make the view clip to the Path provided?
I know it is not helping in clipping but still, we can take the advantage of outline.setConvexPath(path); in elevation shadow since it's working fine as an outline for the elevation shadow around the path.
I attached an image to make it even more clear in which:
First image: ImageView with [Image with shadow]
android:outlineProvider="bounds"
android:elevation="4dp"
Second image: CustomImageView with canvas.clipPath(getShapePath()) in onDraw() method [we will get the clipped image but shadow still show as a rectangle bound]
android:outlineProvider="bounds"
android:elevation="4dp"
Third image: CustomImageView with canvas.clipPath(getShapePath()) in onDraw() method and outline?.setConvexPath(getShapePath()) in ViewOutlineProvider. [we will get the clipped image and shadow will be aligned with the clipped image]
android:outlineProvider="bounds" // this will have no effect since it will be override by setConvexPath
android:elevation="4dp"
Download Source Code
It seems that a View can only be clipped to a rectangle, a rounded rectangle, or an oval Outline. Rather, a View can only be clipped to an Outline that is defined specifically with a setRect(), setRoundRect(), or setOval() method. An Outline set with a Path will not work, even though the Path itself may be one of the aforementioned shapes.
This is documented in several places, though not particularly well, in my opinion.
View#setClipToOutline():
Note that this flag will only be respected if the View's Outline returns true from canClip().
Outline#canClip():
Currently, only Outlines that can be represented as a rectangle, circle, or round rect support clipping.
Defining Shadows and Clipping Views - Clip Views:
Only rectangle, circle, and round rectangle outlines support clipping, as determined by the Outline.canClip() method.
At first look, this didn't seem consistent with Outline's own inner workings, as a convex Path is, in fact, used internally for non-circular ovals. However, it does make sense to simply disallow all externally defined Paths, rather than expend the resources necessary to determine if an arbitrary Path is valid.

Android view clipping/subtracting

Is there any way in Android (from API 15) to clip/subtract views like masking in photoshop?
See the example below:
https://s31.postimg.org/d18lktjq3/index.jpg
The Red view is just a bold funny V shape, while the blue one is something more complex.
Note that in the red view, the striped part is transparent.
The result view i would like to obtain, is something like the blue big view, with the V shape of the second view, and anything above cut away.
Note that, in the final result, the space "inside" the V shape must be transparent.
Currently i achieve this effect using the two views one on top of the other (filling the gap in the V shape view), but this is not optimal because i have to know exactly what the other view is, and the summed up view is bigger than the source.
Thank you
In Android, this is done using Porter-Duff Transfer Modes.
Your best best for this is to have two overlay bitmaps: One that has the red V-shape, and a complementary bitmap that represents everything you want to cut out of the lower layer.
With a custom view, you override onDraw() to do these steps:
Draw the base (blue) bitmap
Draw the red stripe using Porter-Duff mode SRC_OVER
Draw the top V cutout using Porter-Duff mode CLEAR
Code would look something like this (assuming you have created the bitmaps and computed the x,y coordinates where you want to draw them):
Paint paint = new Paint();
paint.setAntiAlias(true);
canvas.drawBitmap(blue_base, blueX, blueY, paint);
// draw the red v on top of the blue part
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
canvas.drawBitmap(red_v, redX, redY, paint);
// erase the unwanted pixels from the blue part
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawBitmap(cut_out, redX, redY, paint);
Here's an interesting tutorial to get you started: Punch a hole in a bitmap by using Android's porter-duff Xfer - TechRepublic

Android canvas draw path with multiple "drawing tools" and eraser vector (avoid bmps and use transparent bg)

I am working on a program that lets you draw highlights, pen and text comment mark-up. I have a working version where I recycle a bmp 3 times then draw it to the canvas.
The issue is my app has 3 of these views in memory, so that is a lot of bmps being created.
In my current solution, I am trying to avoid bmps. I have extended an ImageView and added the following to the Draw event:
`
public override void Draw (Canvas canvas)
{
base.Draw (canvas);
HighlightLayer.Draw(canvas, Width, Height);
PenLayer.Draw(canvas, Width, Height);
TextLayer.Draw(canvas, Width, Height);
}
`
Note that I am using monodroid so it's C# (but nearly the same as Java) and all the "Layer" objects are a subclass of Drawable. Just assume they have draw path commands etc.
Anyway, it sort of works except all the highlights / pen have black boxes around them.
When I draw a line (that's not an eraser), I use PorterDuff.Mode.Src.
I tried to combine the 3 into one bmp instead of recycling it but the issue was that if I had an eraser vector it would erase anything that is already drawn. So an erased pen line would also erase the highlights ...
Is it possible to "freeze" a drawing so that as you continue to draw paths on the canvas and change the PorterDuff mode to clear it will not effect anything marked as freeze?
I know enough to know that the black box I am seeing around my lines on my Drawables is due to the background bmp not being set. The problem is I want them to be layers on top of the ImageView. If they each retained a background then you could not see the Drawables underneath. How can I use a drawable with the background of the "parent ImageView" and avoid the black background it assumes to be there.
Somehow I need the background of the "parent" ImageView to be applied to the Drawables when it's choosing the transparent fill to avoid the default black.
SOLUTION:
Using the suggestion below I created a LayerDrawable and added Drawables for each mark-up tool. In the Drawable, before drawing the lines I added the following code:
public override void Draw (Canvas canvas)
{
canvas.SaveLayer(new RectF(0,0,Bounds.Width(),Bounds.Height()), null, SaveFlags.All);
canvas.DrawColor(Color.Transparent, PorterDuff.Mode.Clear);
GraphicsUtil.DrawVector(canvas, Lines, ToolID, Bounds.Width(), Bounds.Height());
}

Creating a "cut out" layout

Right now, I have it so that my layout consists of a Fragment that takes up the entire screen and displays an image. I want to make it so that an additional View exists on top of it, also taking up the entire screen. On that top layer, I want to be able to color it all black initially, and then create certain spots that are transparent (alpha?) and reveal the image displayed on the fragment behind it. So basically the screen will be all black except for a few spots where the image behind is showing through, which I would determine programmatically. I've looked into a bunch of the graphics and views that Android provides, but have no clue where to start. Is this suited for a SurfaceView if I just want it to be all black with some spots of alpha?
Once I select the correct view to use, I'm assuming that I just override the onDraw() method and then do something like canvas.setBody(black) and then add shapes of alpha to it? Will the shapes correctly affect the background color?
For your masking View, you can make a custom view that can keep track of which areas to unmask (as Rects or something similar) and then draw them in onDraw(Canvas) like this:
public class MaskView extends View {
private Set<Rect> mRects = new HashSet<Rect>();
private Paint mUnmaskPaint = new Paint();
{
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
/**
* Add an unmasking rectangle to this view's background.
*
* #param rect
* a rectangle used to unmask the background
*/
public void addUnmaskRect(Rect rect) {
mRects.add(rect);
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
for (Rect r : mRects) {
canvas.drawRect(r, mUnmaskPaint);
}
}
}
Your Fragment (or whatever is keeping track of the unmask areas) passes Rects to MaskView via addUnmaskRect(Rect). Whenever the view is redrawn (remember to call invalidate() each time you are done passing Rects) it is first filled with black, and then has any rectangles unmask the black background. The coordinates for your rectangles must be set to the coordinate space of the view, but if the underlying image occupies the exact same area it should be relatively simple (you can also look at the View.getLocationInWindow(int[]) method to help you with this as well).

Categories

Resources