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
Related
I'm trying to implement a Waveform visualizer in my app which draws the waveform on a canvas from a real-time audio input. (Screenshot below)
What I need to do is to make it in a way that it can be scrolled horizontally from left to right.
I figured I should use a HorizontalScrollView and make the canvas gradually grow in width so that it spans over the screen width. Now I'm wondering how this can be achieved?
Keep the Canvas's width the same and use the canvas.translate() method when actually drawing.
So, let's say you have a Rect rect that contains the rectangle to draw (whose boundaries may exceed that of the View), as well as int horizontalOffset = 0 as the variable holding the offset of your drawing rectangle which will be adjusted as the user touches the View.
Now, when onDraw(Canvas) is called:
#Override
public void onDraw(Canvas canvas){
super.onDraw(canvas);
canvas.save();
canvas.translate(horizontalOffset, 0);
canvas.drawRect(rect, paint);
canvas.restore();
}
Then you can then override onTouch(View, MotionEvent) to calculate the horizontal offset based on the touch event and then invalidate() the View.
Note: this is clearly only drawing a Rect, but you should be able to modify what I did in there to draw whatever you're drawing. The important parts are (in order) save(), translate(), draw, restore().
I try to do circle menu like in this app.
In "expanded" mode i draw this component like follows:
<RelativeLayout android:id="#+id/bigCircle">
<!--color full borders-->
<my.custom.component android:id="#+id/middleCircle">
<!--circle for buttons-->
<RelativeLayout android:id="#+id/smallCircle">
<!--minus button-->
</RelativeLayout>
</my.custom.component>
</RelativeLayout>
In onDraw method of my.custom.component i divide circle on 8 parts by using android.graphics.Path with android.graphics.Paint and some math.
Visually i have exactly as shown in the screenshot. But when i press on part of circle, i need redraw this part in another color to show user what something going on.
How i can redraw part of component's canvas cutting off from another part of canvas by android.graphics.Path for example. In another word i know what redraw canvas i should do in onDraw method, i know that i can show some bitmap from drawables painted in photoshop and have some "multiscreen trouble", i know how i can determine part which user pressed. But i don't know how i can select part of canvas and redraw it.
Developer of Catch here. If I'm understanding your issue, you're having trouble understanding how to specifically draw the highlight/selection indicator on a section of your circular menu.
While there are plenty of different ways one could implement it, what you're leaning towards (using android.graphics.Path) is how we did it. In the view hierarchy of our capture button, there's an element that serves as the canvas on which the selection highlight color (if there is an active selection) is drawn.
If you had a similar custom View in your layout, you could duplicate this behavior like so. First, you'll need the Path that defines the selection for a particular circle segment. Using Path.addArc(RectF, float, float) we can get the pizza-slice-shaped path we need:
private Path getPathForSegment(float startAngle, float sweep) {
Point center = new Point(getWidth() / 2, getHeight() / 2);
RectF rect = new RectF(0f, 0f, getWidth(), getHeight());
Path selection = new Path();
selection.addArc(rect, startAngle, sweep);
selection.lineTo(center.x, center.y);
selection.close();
return selection;
}
The getWidth() and getHeight() above are for the enclosing custom view object, so they define the bounding box that contains the circle on which the selection is drawn.
Then, in your custom view's onDraw(Canvas), if your code has determined a selection should be drawn for a segment:
#Override
protected void onDraw(Canvas canvas) {
// Assume one has the rest of these simple helper functions defined
if (shouldDrawSelection()) {
float startAngle = getStartAngleOfSelectedSegment();
float sweep = getSweepAngle();
Paint paint = getPaintStyleForSelectedSegment();
Path path = getPathForSegment(startAngle, sweep);
canvas.drawPath(path, paint);
}
// ...
super.onDraw(canvas);
}
In the other areas of your code that are tracking touches, just call invalidate() on the custom view so that it will redraw (or not) the selection path based on changes in input or state.
Remember that it's good practice to avoid newing objects in onDraw(), so most of these building blocks (Paths, Paints, etc.) can be constructed ahead of time (or once, on first occurrence) and reused.
Hope this is close to what you were asking!
I understand how to draw I shape, I am extending the View class, and overriding the onDraw, where I create a new ShapeDrawbale (Rectangle), which I then draw to the canvas:
Rect rect = new Rect(x, y, x + width, y + height);
shapeDrawable.setBounds(rect);
shapeDrawable.getPaint().set(paint);
shapeDrawable.draw(canvas);
I then want to add this to a view defined in my Activities layout xml.
I do this by getting a handle on the view and calling:
innerLinear.addView(rectView); // where rectView is my custom class that extends View
My problem is that when creating the Rectangle, you have to provide X and Y coordinates.
So - how do I get the rectanlge positioned correctly within the innerLayout?
Do I have use the bounds of innerLayout to create the rectangle? If so, when I call innerLayout.getLeft(), or innerLayout.getTop() etc.. 0 is always returned (I presume layout has not yet fully completed), so how do I do this?
Is there another way?
I feel like I'm missing something pretty basic here.
Any help would be greatly appreciated,
thanks
In the onDraw method you can use following apis:
For x and y, the reference point is 0,0 with respect to parent view.
getWidth() // This is in View
getHeight() // This is in View
When you call above apis in your CustomView you will get the right dimensions of the parent view which in your case is innerLayout. Now if I have to cover all the area I ll write something like:
Rect rect = new Rect(0, 0, getWidth(), getHeight());
This will fill your CustomView completely. How your 0, 0 is mapped to the actual screen coordinates, you dont worry about it. As I mentioned, your view's reference point is 0,0. And Android will calculate the actual screen coordinates.
For further ref:
Custom Components
How Android draws
I have created CustomView. The view is higher in size than the screen. So it is showing fully in the device screen.
I want to determine position to determine how much it is outside the screen in left, right top and bottom. How to do that?
For what purpose you want to know how much of your view is outside of the screen? Can't you just use Scroller?
You can call getLocalVisibleRect on the view to get the "on screen" rectangle.
For example say in your onDraw
#Override
protected void onDraw(Canvas canvas) {
Rect r=new Rect();
getLocalVisibleRect(r);
// 0 to r.top is above the visible screen, etc
...
Once you have that rect it should be simple to calculate the values you want.
public class POCII extends Activity {
myView mv = new myView(this);
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(mv);
}
}
class myView extends View {
public myView(Context context) {
super(context);
}
#Override
public void onDraw(Canvas canvas) {
Paint paint = new Paint();
canvas.drawRect(0,0,100,100, paint);
canvas.clipRect(0,0,50,50);
}
}
My question is, shouldn't the above code draw a rectangle and then crop the top left portion? The rectangle is not getting cropped.
Please explain what clipRect does. What is it actually clipping? Does it clip in the form of a rectangle, given the co-ordinates? If so, Why is the above code not working?
Canvas.clipRect(left, top, right, bottom) reduces the region of the screen that future draw operations can write to. It sets the clipBounds to be the spacial intersection of the current clipping rectangle and the rectangle specified. There are lot of variants of the clipRect method that accept different forms for regions and allow different operations on the clipping rectangle. If you want to explicitly set the clipping region, try:
canvas.clipRect(left, top, right, bottom, Region.Op.REPLACE);
The 5th argument means replace the clipping rectangle rather than creating the intersection with the previous version.
Try moving the clipRect statement before the drawRect statement. Or, try adding:
paint.setColor(Color.YELLOW);
drawRect(0,0,75,75);
after your existing clipRect statement. It should draw a 50x50 yellow square over what you had before.
Another note: (after long frustration with the apparently, largely undocumented View/ViewGroup/drawing code) I found that canvas.translate(x,y) also adjusts the clipRect. The interaction of clipRect and the drawing matrix is very confusing. It is helpful to point out:
canvas.getMatrix()
and
canvas.getClipBounds()
before and after modifications to the canvas and before drawing things.
To crop the top left portion, do:
canvas.clipRect(0,0,50,50, Region.Op.DIFFERENCE);
// secondly...
canvas.drawRect(0,0,100,100, paint);
ICS and above ...
XOR, Difference and ReverseDifference clip modes are
ignored by ICS if hardware acceleration is enabled.
Just disable 2D hardware acceleration in your view:
myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Reference Android: Howto use clipRect in API15
your drawing looks like this without using cliprect:
now if we use a cliprect we are putting a overlay of a rectange over what we already have. its sort of invisible. lets say we called the following:
override fun onDraw(canvas: Canvas) {
val paint = Paint();
paint.color = Color.RED
canvas.clipRect(0f,0f,500f,500f, Region.Op.DIFFERENCE);
// secondly...
canvas.drawRect(0f,0f,1000f,1000f, paint);
}
since we use DIFFERENCE option and we know the clipping rectangle is now OVER our canvas red rectangle we can tell me special things. above says we should KEEP the DIFFERENCE between the clipping rectangle and the original. so it will look like this (since i used half of 1000 for clipping rectangle):
and the opposite if we used intersect would look like this:
i'd love to see if someone can make it do rounded corners.