Background: I have a DropShadowFrameLayout in my app, which is a container used just to draw a drop shadow over it's child views. Like a shadow on top and bottom of a ListView, you know.
It is based on this article: http://proandroiddev.blogspot.ru/2012/01/android-design-tip-drop-shadows-on.html
Cyril Mottier also suggests the same approach: http://cyrilmottier.com/2012/06/08/the-making-of-prixing-3-polishing-the-sliding-app-menu/
It simply overrides dispatchDraw(Canvas canvas) to draw a shadow bitmap over it's children.
I have a VerticalViewPager (https://github.com/castorflex/VerticalViewPager) inside of a DropShadowFrameLayout, and the problem is that it deepens the view hierarchy (which is already complex), causing scrolling glitches.
What I tried: I decided to get rid of that DropShadowFrameLayout by moving shadow-drawing code directly to dispatchDraw(Canvas canvas) of a ViewPager. And the problem here is that ViewPager draws shadow only on the one initially visible child, and when I scroll it, the shadow moves along with it. Of course this is not the behavior I expect, because shadows must be still while the child views move.
This method worked well with a ListView.
The question is: how do I apply this method to a ViewPager? Or is there another way of doing this, which is equal performance-wise? Or, at least, why does the ViewPager behave this way?
The problem was that shadows in DropShadowFrameLayout were always drawn at the fixed position:
private Rect getDropShadowArea(Canvas canvas, Bitmap bm) {
return new Rect(0, 0, canvas.getWidth(), bm.getHeight());
}
ViewPager turned out to use his inner coordinates, so the solution was to adjust this method to be aware of current scroll position of ViewPager (I use Y instead of X because I actually use VerticalViewPager, but the principle is the same for both):
private Rect getDropShadowArea(Canvas canvas, Bitmap bm) {
return new Rect(0, getScrollY(), canvas.getWidth(),
getScrollY() + bm.getHeight());
}
Or you can just translate the canvas by getScrollY() before drawing, if you like (thanks to pskink).
Also the scrolling lag was (partly) caused by getting the Bitmap every time in the dispatchDraw().
Related
i am implementing a custom launcher in Android, displaying 3rd party apps using ActivityViews.
I want to clip the content of these ActivityViews to a custom shape (other than Circle, Rectangle, RoundedRectangle, Ring...).
I already tried to call clipPath/drawPath on the dispatchDraw canvas of a parent viewgroup element which works fine for all children except the ActivityView. Even the ActivityView itself and its referenced SurfaceView seem to be clipped according to my given path (if i add a solid color for testing). But the rendered content remains unchanged. Manipulating the SurfaceView canvas (which you receive by calling getHolder().lockCanvas()) doesnt have any effect, too.
I think this has something to do with the virtualdisplay and/or various SurfaceControls which are used by the ActivityView, but i dont have any clue how to set clipping areas/paths for those classes.
Does anyone have an idea how to solve this?
Hint: i cannot paint over the ActivityViews content as i want to display the system wallpaper in the transparent areas.
finally setting up the PorterDuffXfermode correctly solved the issue. Just overwrite the dispatchDraw method of the parent viewgroup and erase the required areas using PorterDuff.
Sometimes it looks like the direction does matter how the path for drawPath is created (CW or CCW). So maybe try both.
Unfortunately this approach does not work if the hosted activity uses a SurfaceView on its own.
protected void dispatchDraw(Canvas canvas) {
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(getContext().getColor(android.R.color.white));
p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
int save = canvas.save();
super.dispatchDraw(canvas);
canvas.drawPath(path, p);
canvas.restoreToCount(save);
}
I'm trying to override onDraw in an EditText subclass, to show a custom subtitle.
I've got it working, but there are a few bugs.
Basicall, all I need to do is draw StaticLayout at a certain offset from top left corner of the view.
Unfortunantly, all I get in my onDraw method is canvas. The size of the canvas is equal to the size of the whole screen (320x480 on a device with 320x480 display) and its clip bounds can be pretty much anything - it can be the whole view; it can be only top or bottom part of the view if the view is inside scrollview and partially visible; it can even be same arbitrary rect inside the view, probably because superclass invalidates only some of its region.
So if I have this view with size 320x48, I can get canvas with size 320x480 and clipping rect (200, 200, 300, 230) (left, top, right, bottom). I don't understand how this clipping rect maps to my view coordinates.
I need to know where is top left corner of the clipping rect relative to the top left corner of the view. Unfortunantly, I cannot figure out how to get this.
Added:
This code will work on all os versions that I've tested:
private int[] coordinates = new int[2];
private Matrix identityMatrix = new Matrix();
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
getLocationInWindow(coordinates);
canvas.setMatrix(identityMatrix);
canvas.translate(coordinates[0], coordinates[1]);
//do the drawing in EditText coordinate space
canvas.restore();
}
However, I still have one question: why does it work?
I've trying overriding View class and it's onDraw method will always recieve a canvas which size matches that of the View itself. Canvas for direct View subclass will have no clipping rect. Same for TextView (direct ancestor of the EditText class). But it's not the same for EditText. Canvas that gets passed to onDraw method of EditText will always (or not?) have the size of the screen, and a custom clipping rect. This whole "translate by view coordinates in window thing" seems very hacky. I don't understand why I should translate the coordinate space.
I've tried hacking android source code for the answers, but found none. EditText has no onDraw of its own. Theoretically, there should be no difference between overriding TextView onDraw and EditText onDraw. But there is a difference. The canvas object passed to onDraw method will be different depending on whether its TextView or EditText. Why? How do I know when I should apply transformation to matrix, and when I shouldn't?
you can have it with the View.getLocationOnScreen method
I got success in implementing a pinch zoom in/out and drag/drop functionality in images on the canvas.
Now what I want is re-sizing, that images like below link that based on iPhone App
How to change shape of an image using iPhone SDK?
So how can I achieve that kind of functionality in Android ?
Basically you need to invalidate the image and re-draw on the canvas from the beginning ::
img=(ImageView)findViewById(R.id.yourimageidfromxml);
img.onTouchEvent(MotionEvent me)
{
int X=me.getX();
int Y=me.getY();
img.invalidate();
img.repaint(X,Y);
}
void paint(int X,int Y)
{
img.setWidth(X);
img.setHeight(Y);
}
Scaling image will transform using repaint on canvas from the beginning
If by re-sizing you are referring to "stretching" the Bitmap on the vertical and horizontal plane then you simply modify the rect that the shape (eg. oval) is being drawn into.
For example:
This is your original shape of an oval:
canvas.drawOval(new Rect(0,0,100,100), bluePaint);
This is the same oval, just stretched (resized) on the horizontal plane:
canvas.drawOval(new Rect(0,0,200,100), bluePaint);
I hope this helps.
There are two options, both involving a custom view. The first is to create a custom view that fills your "canvas". You can keep track of 8 blue and 1 green circles in the view as Rect objects. Override onTouchEvent(MotionEvent) and then check if motion events are in any of your controls and update them accordingly (I'm simplifying things a bit here :)). From your onTouchEvent you would call invalidate(). You're onDraw(Canvas) can then handle drawing the controls and update the image according to how your controls have changed since the last call to onDraw.
The other option is to do something similar, but with a view that only encapsulates the circle and controls, meaning that moving the view around would require a container that will let the view change it's layout parameters. Doing this, your onTouchEvent method would need to trigger an invalidate with that layout view because it would need to recalculate the size and position of your view. This would definitely be harder but depending on what you are trying to achieve working with individual views may be better than maintaining representations of your circles in code in a single view.
The resizing of the image can be achieved by using a simple ImageView with scaleType "fitXY".
You have to add the blue resize handles yourself.
Changing the rotation of the image (green handle) can be achieved by using:
public static Bitmap rotate(Bitmap src, float degree) {
// create new matrix
Matrix matrix = new Matrix();
// setup rotation degree
matrix.postRotate(degree);
// return new bitmap rotated using matrix
return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true);
}
Source: http://xjaphx.wordpress.com/2011/06/22/image-processing-rotate-image-on-the-fly/
See http://xjaphx.wordpress.com/learning/tutorials/ for more Android Image Processing examples.
I've seen a few people ask how to zoom an entire ViewGroup (such as a RelativeLayout) in one go. At the moment this is something I need to achieve. The most obvious approach, to me, would be to hold the zoom scale factor as a single variable somewhere; and in each of the child Views' onDraw() methods, that scale factor would be applied to the Canvas prior to graphics being drawn.
However, before doing that, I have tried to be clever (ha - usually a bad idea) and extend RelativeLayout, into a class called ZoomableRelativeLayout. My idea is that any scale transformation could be applied just once to the Canvas in the overridden dispatchDraw() function, so that there would be absolutely no need to separately apply the zoom in any of the child views.
Here's what I did in my ZoomableRelativeLayout. It's just a simple extension of RelativeLayout, with dispatchDraw() being overridden:
protected void dispatchDraw(Canvas canvas){
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.scale(mScaleFactor, mScaleFactor);
super.dispatchDraw(canvas);
canvas.restore();
}
The mScaleFactor is manipulated by a ScaleListener in the same class.
It does actually work. I can pinch to zoom the ZoomableRelativeLayout, and all of the views held within properly rescale together.
Except there's a problem. Some of those child views are animated, and hence I periodically call invalidate() on them. When the scale is 1, those child views are seen to redraw periodically perfectly fine. When the scale is other than 1, those animated child views are only seen to update in a portion of their viewing area - or not at all - depending on the zoom scale.
My initial thinking was that when an individual child view's invalidate() is being called, then it's possibly being redrawn individually by the system, rather than being passed a Canvas from the parent RelativeLayout's dispatchDraw(), meaning that the child view ends up refreshing itself without the zoom scale applied. But oddly, the elements of the child views that are redrawn on the screen are to the correct zoom scale. It's almost as if the area that the system decides to actually update in the backing bitmap remains unscaled - if that makes sense. To put it another way, if I have a single animated child View and I gradually zoom in further and further from an initial scale of 1, and if we place an imaginary box on the area where that child view is when the zoom scale is 1, then the calls to invalidate() only cause a refresh of the graphics in that imaginary box. But the graphics that are seen to update are being done to the right scale. If you zoom in so far that the child view has now moved completely away from where it was with a scale of 1, then no part of it at all is seen to refresh. I'll give another example: imagine my child view is a ball that animates by switching between yellow and red. If I zoom in a little bit such that the ball moves to the right and down, at a certain point you'll just see the top-left quarter of the ball animate colours.
If I continuously zoom in and out, I see the child views animate properly and entirely. This is because the entire ViewGroup is being redrawn.
I hope this makes sense; I've tried to explain as best as I can. Am I on a bit of a loser with my zoomable ViewGroup strategy? Is there another way?
Thanks,
Trev
If you are applying a scale factor to the drawing of your children, you also need to apply the appropriate scale factor to all of the other interactions with them -- dispatching touch events, invalidates, etc.
So in addition to dispatchDraw(), you will need to override and appropriate adjust the behavior of at least these other methods. To take care of invalidates, you will need to override this method to adjust the child coordinates appropriately:
http://developer.android.com/reference/android/view/ViewGroup.html#invalidateChildInParent(int[], android.graphics.Rect)
If you want the user to be able to interact with the child views you will also need to override this to adjust touch coordinates appropriately before they are dispatched to the children:
http://developer.android.com/reference/android/view/ViewGroup.html#dispatchTouchEvent(android.view.MotionEvent)
Also I would strongly recommend you implement this all inside of a simple ViewGroup subclass that has a single child view it manages. This will get rid of any complexity of behavior that RelativeLayout is introducing in its own ViewGroup, simplifying what you need to deal with and debug in your own code. Put the RelativeLayout as a child of your special zooming ViewGroup.
Finally, one improvement to your code -- in dispatchDraw() you want to save the canvas state after applying the scaling factor. This ensures that the child can't modify the transformation you have set.
The excellent answer from hackbod has reminded me that I need to post up the solution that I eventually came to. Please note that this solution, which worked for me for the application I was doing at the time, could be further improved with hackbod's suggestions. In particular I didn't need to handle touch events, and until reading hackbod's post it did not occur to me that if I did then I would need to scale those as well.
To recap, for my application I what I needed to achieve was to have a large diagram (specifically, the floor layout of a building) with other small "marker" symbols superimposed upon it. The background diagram and foreground symbols are all drawn using vector graphics (that is, Path() and Paint() objects applied to Canvas in the onDraw() method). The reason for wanting to create all the graphics this way, as opposed to just using bitmap resources, is because the graphics are converted at run-time using my SVG image converter.
The requirement was that the diagram and associated marker symbols would all be children of a ViewGroup, and could all be pinch-zoomed together.
A lot of the code looks messy (it was a rush job for a demonstration) so rather than just copying it all in, instead I'll try to just explain how I did it with the relevant bits of code quoted.
First of all, I have a ZoomableRelativeLayout.
public class ZoomableRelativeLayout extends RelativeLayout { ...
This class includes listener classes that extend ScaleGestureDetector and SimpleGestureListener so that the layout can be panned and zoomed. Of particular interest here is the scale gesture listener, which sets a scale factor variable and then calls invalidate() and requestLayout(). I'm not strictly certain at the moment if invalidate() is necessary, but anyway - here it is:
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector){
mScaleFactor *= detector.getScaleFactor();
// Apply limits to the zoom scale factor:
mScaleFactor = Math.max(0.6f, Math.min(mScaleFactor, 1.5f);
invalidate();
requestLayout();
return true;
}
}
The next thing I had to do in my ZoomableRelativeLayout was to override onLayout(). To do this I found it useful to look at other people's attempts at a zoomable layout, and also I found it very useful to look at the original Android source code for RelativeLayout. My overridden method copies much of what's in RelativeLayout's onLayout() but with some modifications.
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
int count = getChildCount();
for(int i=0;i<count;i++){
View child = getChildAt(i);
if(child.getVisibility()!=GONE){
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)child.getLayoutParams();
child.layout(
(int)(params.leftMargin * mScaleFactor),
(int)(params.topMargin * mScaleFactor),
(int)((params.leftMargin + child.getMeasuredWidth()) * mScaleFactor),
(int)((params.topMargin + child.getMeasuredHeight()) * mScaleFactor)
);
}
}
}
What's significant here is that when calling 'layout()' on all the children, I'm applying the scale factor to the layout parameters as well for those children. This is one step towards solving the clipping problem, and also it importantly correctly sets the x,y position of the children relative to each other for different scale factors.
A further key thing is that I am no longer attempting to scale the Canvas in dispatchDraw(). Instead each child View scales its Canvas after obtaining the scale factor from the parent ZoomableRelativeLayout via a getter method.
Next, I shall move onto what I had to do within the child Views of my ZoomableRelativeLayout. There's only one type of View I contain as children in my ZoomableRelativeLayout; it's a View for drawing SVG graphics that I call SVGView. Of course the SVG stuff is not relevant here. Here's its onMeasure() method:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
float parentScale = ((FloorPlanLayout)getParent()).getScaleFactor();
int chosenWidth, chosenHeight;
if( parentScale > 1.0f){
chosenWidth = (int) ( parentScale * (float)svgImage.getDocumentWidth() );
chosenHeight = (int) ( parentScale * (float)svgImage.getDocumentHeight() );
}
else{
chosenWidth = (int) ( (float)svgImage.getDocumentWidth() );
chosenHeight = (int) ( (float)svgImage.getDocumentHeight() );
}
setMeasuredDimension(chosenWidth, chosenHeight);
}
And the onDraw():
#Override
protected void onDraw(Canvas canvas){
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.scale(((FloorPlanLayout)getParent()).getScaleFactor(),
((FloorPlanLayout)getParent()).getScaleFactor());
if( null==bm || bm.isRecycled() ){
bm = Bitmap.createBitmap(
getMeasuredWidth(),
getMeasuredHeight(),
Bitmap.Config.ARGB_8888);
... Canvas draw operations go here ...
}
Paint drawPaint = new Paint();
drawPaint.setAntiAlias(true);
drawPaint.setFilterBitmap(true);
// Check again that bm isn't null, because sometimes we seem to get
// android.graphics.Canvas.throwIfRecycled exception sometimes even though bitmap should
// have been drawn above. I'm guessing at the moment that this *might* happen when zooming into
// the house layout quite far and the system decides not to draw anything to the bitmap because
// a particular child View is out of viewing / clipping bounds - not sure.
if( bm != null){
canvas.drawBitmap(bm, 0f, 0f, drawPaint );
}
canvas.restore();
}
Again - as a disclaimer, there are probably some warts in what I have posted there and I am yet to carefully go through hackbod's suggestions and incorporate them. I intend to come back and edit this further. In the meantime, I hope it can start to provide useful pointers to others on how to implement a zoomable ViewGroup.
Ok, here's the deal. I want to move items in my extended gallery class to change the order of images. The way i'm doing it now:
on long press remove the current selected item,
use onDraw() to draw the same image so i can move it around using onTouchEvent()
on release add the item again
This works fine but the problem is that when using the onDraw() method it will draw the image behind the gallery items. Is there a way to change the priority of what is drawn?
Well i found this out after going into a totally different direction =/
Here's the solution for people that have the same problem:
In constructor (or anywhere else you initialize the component) set setWillNotDraw(false) and override dispatchDraw(). dispatchDraw() draws the ViewGroup children so you can decide yourself if you want to draw behind or a top of the other views.
Example taken from Custom drawing on top of Gallery view (and it's child views)
#Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// do your drawing stuff here
canvas.drawPath(mPath,mPaint);
}