How to implement this image stack effect in Android? - android

I looked at the H&M Android app and trying to figure out how to implement some widget.
Can anyone have an idea how this image frame is implemented?
I can guess that it using openGL.

A transparent png frame? Which could also be nine-patch!

I will venture a guess ;)
First the front image is created. In this case, it is built by inflating a linear layout with an ImageView and a TextView. Then this is cached to a Bitmap (at setup phase, not draw time).
In onDraw, that bitmap is drawn to the screen. Then the canvas is clipped to avoid drawing that area any more. Saves a lot of drawing time to not do a quadruple overdraw of transparent pixels.
Then the backgrounds are drawn like this:
for(int i = NUMBER_OF_LAYERS - 1; i > 0; i--) {
canvas.save();
float rotation = MAX_ANGLE * shiftModifier * ((float) i / (NUMBER_OF_LAYERS - 1));
canvas.rotate(rotation, mImageHalfWidth, mImageHalfHeight);
paint.setAlpha((int) (255f / (2 * i)));
canvas.drawRect(mBitmap.getBounds(), paint);
canvas.restore();
}
NUMBER_OF_LAYERS is the number of backgrounds layers.
MAX_ANGLE is the rotation angle for the most tilted layer.
shiftModifier is used to animate the background layers. It moves from zero (background completely hidden) to one (background angle = MAX_ANGLE).
paint is just a Paint with color set to white.

Related

How to scale thickness or strokeWidth of line when screen size changes

I am playing around with an application that draws and traces the user's finger on the canvas. See this picture, the green rectangle was drawn by tracing the user's finger motion. I took most of the code from the old FingerPaint application from the android SDK samples. The gist of it is to draw a Path on a canvas with:
canvas.drawPath (path, paint)
My first challenge was to redraw the path correctly when the screen changes. For example, when the phone gets rotated from vertical to horizontal, the image gets resized and I have to redraw the path along the correct coordinates. I was able to correctly do that with transform. I used the path.transform(matrix) to translate the coordinates of the path.
My next challenge now is to properly scale the thickness of the line. When I draw the path, the thickness of line is set with setStrokeWidth. For example, I set the thickness to 12:
paint.setStrokeWidth(12);
I don't know how to properly scale the thickness of the line when the screen changes. Would anynone know how to do that ?
A suggestion would be to first save the canvas and the path as a bitmap and then redraw the bitmap on the new screen dimensions. But I don't want to do that because I want to support a undo/redo operations where the user can undo/redo the paths that were traced on the canvas.
you can use sth like this:
float screenWidthPixel = this.getResources().getDisplayMetrics().widthPixels;
float screenHeightPixel = this.getResources().getDisplayMetrics().heightPixels;
float STROKE_WIDTH = screenWidthPixel * 0.0035f;
float Y_POSITION = screenHeightPixel * 0.01f;
paint.setStrokeWidth(STROKE_WIDTH);

Android - Fill Path with color partially

I am trying to draw a heart shaped Canvas using Path in Android. The code is as follows :
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Fill the canvas with background color
canvas.drawColor(Color.WHITE);
// paint.setShader(null);
// Defining of the heart path starts
path.moveTo(left + WIDTH / 2, top + HEIGHT / 4); // Starting point
// Create a cubic Bezier cubic left path
path.cubicTo(left+WIDTH/5,top,
left+WIDTH/4,top+4*HEIGHT/5,
left+WIDTH/2, top+HEIGHT);
// This is right Bezier cubic path
path.cubicTo(left + 3 * WIDTH / 4, top + 4 * HEIGHT / 5,
left + 4 * WIDTH / 5, top,
left + WIDTH / 2, top + HEIGHT / 4);
paint.setShader(new LinearGradient(0, canvas.getHeight()/4, canvas.getWidth(), canvas.getHeight()/4, new int[]{Color.RED, Color.YELLOW, Color.GREEN}, new float[]{0, 0.6f, 1}, Shader.TileMode.CLAMP));
canvas.drawPath(path, paint);
heart_outline_paint.setColor(getResources().getColor(R.color.heart_outline_color)); // Change the boundary color
heart_outline_paint.setStrokeWidth(4);
heart_outline_paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path, heart_outline_paint);
}
I am able to draw heart without any issue and I am able to fill color inside the heart using the Fill option in Paint. But I should be able to fill the heart dynamically according to some data and it cannot be filled fully all the time. What I have achieved so far is as follows :
I have made an extensive search and came across a lot of things similar to this. Some of which includes :
Android fill in part of a path?
filling a circle gradually from bottom to top android
I also came across the concept of converting the canvas to bitmap and filling color inside the bitmap using Flood Fill Algorithm which lets users to fill colors inside the bitmap. However, I do not want the bitmap to fill the color while touching inside the heart but to fill while a button click action.
I thought that filling a circle gradually from bottom to top android
would give help me but it makes use of a circle and I am not well-versed in Canvas which makes me very weak in adapting the circle code to such a shape.
If anybody has some ideas or any insights on how to achieve this, it will be really helpful. Cheers. Thanks in advance.
P.S : I also tried some tricks using setShader in Paint but nothing would give me what I want.
EDIT :
I just stumbled upon a idea of drawing a rectangle over the heart with another color same as the background of the canvas so that it will look like its half filled !! I am still working on the idea and not sure how accurate this is gonna be for me. If someone has a better idea, you're most welcome.
I used clipPath function available in Canvas to achieve what I needed. I draw the heart by above method and draw a rectangle over it, and I use the clipPathfunction to clip out the region that is outside the heart.
public static double filled_amount = .90;
path.moveTo(left_x_moveto, left_y_moveto);
path.cubicTo(left_x1, left_y1, left_x2, left_y2, left_x3, left_y3);
path.cubicTo(right_x2, right_y2, right_x1, right_y1, left_x_moveto, left_y_moveto);
path.close();
Rect rect = new Rect((int)(canvas.getWidth()*.10),(int)(canvas.getHeight()*filled_amount),(int) canvas.getWidth(), (int) canvas.getHeight());
canvas.clipPath(path);
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.FILL);
canvas.drawPath(path, paint);
canvas.drawRect(rect, rect_paint);
heart_outline_paint.setColor(getResources().getColor(R.color.heart_outline_color)); // Change the boundary color
heart_outline_paint.setStrokeWidth(15);
heart_outline_paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path, heart_outline_paint);
This will give me the desired result of filling the heart dynamically. Changing the value of filled_amount dynamically and calling invalidate() will make it look like the heart is being filled dynamically.
#Henry's answer might be a better one but this did the trick for me and I dont look deeply in to the edges so a bit of zig-zags here and there is all right.
You could use Bitmap Masking to get a partially filled Heart. What you ideally do here is use one bitmap to mask the other.
In your case you could have a filled rectangle in the canvas and you have then have the heart shape in a new bitmap to act as the mask. You could then dynamically change the filling of the heart by changing the height of the background rectangle.
Refer this: https://stackoverflow.com/a/33483600/4747587. This contains the implementation of partially filling a Star. The idea is the same.

Paths drawn from the same bounding box have different radii

Background
I'm drawing a custom View, which consists of an arc along which images are drawn.
A bit like this "Wheel of Fortune" screenshot, where only part of a large disc is visible and, as the user drags the view, images become visible/hidden as appropriate and are drawn at the appropriate position and angle along the disc's edge.
This works fine; I use the code below to create a large bounding box (four times the width of the view, to get a more subtle arc), which I use with Path.arcTo() to draw the visible top edge of the disc.
Because the bounding box is square, the arc drawn (if I were to draw 360°) would be circular.
// Disc dimensions (based on this View's width/height/padding)
final int radius = width * 2;
final float halfWidth = width / 2f;
final float top = mTopPadding;
// Create a large, square bounding box to draw the disc in.
// Centre horizontally; top edge of the disc == top edge of this View (+ padding)
final RectF discBounds =
new RectF(-radius + halfWidth, top, radius + halfWidth, radius * 2 + top);
// Create an arc along the circumference of the disc,
// but only where it will intersect with this View
double arcSweep = Math.toDegrees(Math.asin(halfWidth / radius)) * 2;
double startAngle = 180 + ((180 - arcSweep) / 2d);
mDiscPath.reset();
mDiscPath.arcTo(discBounds, (float) startAngle, (float) arcSweep);
// Close the shape so that we fill the rest of this View
// (the area underneath the arc) with the disc bg colour
mDiscPath.lineTo(width, height);
mDiscPath.lineTo(0, height);
I then create another Path and again call arcTo(), using the exact same bounding box so that the same arc radius is maintained.
This time the sweep angle of the arc is longer, since there may be only two or three images shown within the View at one time, but an arbitrary number of images off-screen (in my case, up to about ten).
// Create another arc, along which the images should move,
// based on the number and width of the images.
// We will later use a PathMeasure object created from
// this Path to determine where to draw each image
arcSweep = (mTotalWidth * 180) / (radius * Math.PI);
startAngle = 180 + ((180 - arcSweep) / 2d);
mImagePath.reset();
mImagePath.arcTo(discBounds, (float) startAngle, (float) arcSweep);
Problem
In onDraw(), the mDiscPath is drawn as the background (canvas.drawPath(mDiscPath, fillPaint)), and then the appropriate bitmaps are drawn based on a PathMeasure object created from mImagePath and how far the user has dragged.
However, it's noticeable that the images do not precisely follow the expected path as the disc is "rotated". This causes problems, as the images need to align accurately to the edge of the disc.
For troubleshooting, I started drawing mImagePath using canvas.drawPath(mImagePath, strokePaint)) to see why the image path didn't seem to follow the disc path.
In the screenshot below, to make the problem more obvious, the regular bitmaps are not drawn on top of the disc, and mImagePath was translated downwards by 4dp (i.e. the problem is also visible when not translated).
Here we can see three independent instances of the custom View stacked on top of one another.
But it's clear that the black line (mImagePath) does not match the radius of the top of the coloured disc (mDiscPath) in each case. i.e. The radius of the black arc appears to be large than the disc's radius.
The arcs for both Path objects were created using the same bounding box, so I would expect both arcs to have the same radius.
The line on the bottom disc seems to match up well, but the top two discs are clearly wrong.
The only real difference between the discs is the number of images displayed, and therefore the sweep angle of the image path (89°, 169°, 222° respectively for the three views).
Question
Why, if the exact same square RectF bounding box is being used to create two Path objects, why do arcs drawn from these Paths have different radii?
Am I missing something? Should I be using a different API?
Postscript
I've ensured the bounding box is correctly sized and doesn't change between creating the two paths.
The start and sweep angles look correct in all cases (i.e. the midpoint of each arc is at 270°).
Creating brand new Paths or resetting the existing Paths makes no difference.
Using the same arc sweep for both Paths does work as expected.
I've tested on various devices and orientations, with and without software rendering.

Working with big size drawable and custom views in Android

I know this has been discussed over and over again but I can't find a solution to suit my needs.
Scenario: To keep the explanation simple, I have a custom view which displays two images a big one for the background and a smaller one as a Wheel. The user can rotate/scale the background image with onTouch events. He also can rotate the Wheel image to make some operations. Everything is done on a custom View not SurfaceView because I need it to be transparent.
The problem: With onDraw() I always need to check what Rotation/Scale has the background image, scale/rotate it and then draw it. If the background image is smaller, let's say 512x512 the Wheel image rotation is fine. If the background image is bigger, 1280x1707, the wheel image rotation is laggy. So my guess is, manipulation and rotation of a big image in background, for each onDraw() gives me performance issues, when basically the background image should be redrawn only the the user manipulates it.
The rotation is done in something like:
canvas.save();
float dx = (maxX + minX) / 2;
float dy = (maxY + minY) / 2;
drawable.setBounds((int) minX, (int) minY, (int) maxX, (int) maxY);
canvas.translate(dx, dy);
canvas.rotate(angle * 180.0f / (float) Math.PI);
canvas.translate(-dx, -dy);
drawable.draw(canvas);
canvas.restore();
Possible solutions: I could make a new custom View which could only draw the background image and on top of it, put my current View and send touch events, when the case to my background image view. This will allow me to redraw the background image only when needed. Any other ideas ?
I was having similar performance problems and the solution I found is what I described here. Similar to what you already suggested, basically the background is only drawn once and the custom ImageView handles all transformations on its touch events.

Using a gradient along a path

I'm trying to create a 'glow' effect using the Android Path class. However, the gradient is not being warped to fit around the path. Instead, it is simply being display 'above' it and clipped to the path's stroke. Using a square path, the image below shows what I mean:
Instead, that should look more like this:
In other words, the gradient follows the path, and in particular wraps around the corners according to the radius set in the CornerPathEffect.
Here is the relevant part of the code:
paint = new Paint();
paint.setStyle(Style.STROKE);
paint.setStrokeWidth(20);
paint.setAntiAlias(true);
LinearGradient gradient = new LinearGradient(30, 0, 50, 0,
new int[] {0x00000000, 0xFF0000FF, 0x00000000}, null, Shader.TileMode.MIRROR);
paint.setShader(gradient);
PathEffect cornerEffect = new CornerPathEffect(10);
paint.setPathEffect(cornerEffect);
canvas.drawPath(boxPath, paint);
Any ideas?
Another alternative is to get a 'soft-edged brush' effect when defining the stroke width. I've experimented with BlurMaskFilters, but those give a uniform blur rather than a transition from opaque to transparent. Does anyone know if that's possible?
How about drawing with a soft brush bitmap? Make a soft circular brush with opacity decreasing radially outward using image editing software like Photoshop. Save as drawable, load it in a bitmap and draw it evenly spaced along your path. Make the bitmap with white coloured brush. This way you can simply multiply the given colour(Here blue) to your bitmap using PorterDuffColorFilter.
brush1=BitmapFactory.decodeResource(getResources(), R.drawable.brush_custom_one);
//This contains radially decreasing opacity brush
porter_paint.setColorFilter(new PorterDuffColorFilter(paint.getColor(), Mode.MULTIPLY));
for (int i=1;i<matrix.size();i++) {
//matrix contains evenly spaced points along path
Point point = matrix.get(matrix.get(i));
canvas.drawBitmap(brush1, point.x,point.y, porter_paint);}
The brush used is (It's there):
The final result is:
Turns out there was a stupidly obvious way of doing this. Simply re-use the same path, and adjust the stroke width and alpha on each drawing pass. Example code:
float numberOfPasses = 20;
float maxWidth = 15;
for (float i = 0; i <= numberOfPasses; i++){
int alpha = (int) (i / numberOfPasses * 255f);
float width = maxWidth * (1 - i / numberOfPasses);
paint.setARGB(alpha, 0, 0, 255);
paint.setStrokeWidth(width);
canvas.drawPath(path, paint);
}
See below for an example of the result. The left path was drawn using this method, the right path, for comparison, is drawn in a single stroke with maxWidth and 255 alpha.
This mainly works. There are two problems:
The gradient isn't as smooth as it could be. This is because each pass being drawn over the previous one results in the alpha building up too quickly, reaching 255 before the final strokes. Experimenting a bit with the line int alpha = (int) (i / numberOfPasses * 125f); (note the change to 125f rather than 255f) helps.
The path looks like it has been 'cut' on the insides of the corners. Probably some result of the CornerPathEffect applied.
What you're wanting to do, if I understand it right, is to have the gradient effectively form a "brush" for the stroke.
This is exactly what I also was trying to achieve recently, but as far as I can tell the API doesn't provide any straightforward means to do it. I have recently created an SVG to Android Canvas converter class and so I am working a lot in Inkscape lately, too. So, when I was looking into it, I wondered if it's even possible to do it in Inkscape. However, even in Inkscape it's a very non-trivial thing to do. After some searching I eventually came across this image of a gradient being applied along the course of a path, together with a download link for a tutorial beneath:
http://www.flickr.com/photos/35772571#N03/3312087295/
What I was personally trying to do at the time was to create some semi-circles where the path is a kind of neon glow as opposed to a flat colour. Talking in terms of both the Android API and the SVG standard, it seems that the only way to to do this is to create a radial gradient that's centred perfectly on the circle, and position a series of color stops in exactly the right places. Pretty tricky to do, and I certainly don't know how you'd do it to a shape like a square.
Sorry that this is a bit of a 'I couldn't do it either' rather than a useful answer! I'll follow this with interest as I'm eager to know a solution for a kind of 'soft brush' effect too.
Can be very complicated to draw a gradient than follow a path.
So I suggest you to use some library already done than make it for you.
One can be Sc-Gauges.
Have some usefully classe than you can use for your goal.
For first include the library:
dependencies {
...
compile 'com.github.paroca72:sc-gauges:3.0.7'
}
After create an image or what you want with a canvas where draw:
<ImageView
android:id="#+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
Now the code:
// Dimensions
int padding = 24;
Rect drawArea = new Rect(padding, padding, 700 - padding, 500 - padding);
// Get the main layout
ImageView imageContainer = (ImageView) this.findViewById(R.id.image);
assert imageContainer != null;
// Create a bitmap and link a canvas
Bitmap bitmap = Bitmap.createBitmap(
drawArea.width() + padding * 2, drawArea.height() + padding * 2,
Bitmap.Config.ARGB_8888
);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.parseColor("#f5f5f5"));
// Create the path building a bezier curve from the left-top to the right-bottom angles of
// the drawing area.
Path path = new Path();
path.moveTo(drawArea.left, drawArea.top);
path.quadTo(drawArea.centerX(), drawArea.top, drawArea.centerX(), drawArea.centerY());
path.quadTo(drawArea.centerX(), drawArea.bottom, drawArea.right, drawArea.bottom);
// Feature
ScCopier copier = new ScCopier();
copier.setPath(path);
copier.setColors(Color.RED, Color.GREEN, Color.BLUE);
copier.setWidths(20);
copier.draw(canvas);
// Add the bitmap to the container
imageContainer.setImageBitmap(bitmap);
And this the result:
The first part of the code is just for create a bitmap where draw.
What you interest is the second part where use ScCopier.
Just give the path, the color and the with.
Note than is you are inside a view you can use onDraw for draw directly on the view canvas.
This library can used to create gauge of every kind.
If you want take a look to this site ScComponents have some free and not gauges components.

Categories

Resources