I used this code to draw text vertically.
RectF rectF2 = new RectF();
matrix.mapRect(rectF2, bounds);
canvas.save();
canvas.rotate(90, rectF2.right, rectF2.top);
canvas.drawText(text, rectF2.left, rectF2.bottom, mTextPaint);
canvas.restore();
This works well, but I want to change the coordinates as well. Because later I tap on the object and do the drag and drop.
Now the problem is, As you see in the following image, the coordinates are drawn as rectangle. So when I tap on that rectangle area can only be able to move around the text on canvas.
So I want to rotate the original coordinates as well when I rotate the canvas. I tried matrix.setRotate But I can't able to achieve what I want.
In onDraw(), you can transform your canvas using a matrix.
And in onTouch(), you can transform-back the screen coordinates using the matrix inverse.
private static class MyView extends View {
private final Paint texPaint = new Paint(){{
setTextSize(60);
setColor(Color.BLACK);
}};
private final Paint rectPaint = new Paint(){{
setStrokeWidth(20);
setColor(Color.RED);
setStyle(Paint.Style.STROKE);
}};
private final Matrix matrix = new Matrix();
private final RectF rect = new RectF(0, 0, 400, 150);
public MyView(Context context) {
super(context);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
matrix.reset();
matrix.postRotate(45);
matrix.postScale(1.5f, 1.5f, 0, 0);
matrix.postTranslate(w/2, h/2);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.concat(matrix);
canvas.drawRect(rect, rectPaint);
//Bottom line of the drawn text is at y, if you want the text to inside the rect
// increase the y by the text height, textHeight is the textSize
canvas.drawText("SomeText", 0, texPaint.getTextSize(), texPaint);
canvas.restore();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
float x = event.getX();
float y = event.getY();
Toast.makeText(getContext(), isInsideRegion(x, y) ? "Inside" : "Outside", Toast.LENGTH_SHORT).show();
}
return true;
}
private boolean isInsideRegion(float x, float y) {
Matrix invertedMatrix = new Matrix();
if (!matrix.invert(invertedMatrix)) {
throw new RuntimeException("Matrix can't be inverted");
}
float[] point = {x, y};
invertedMatrix.mapPoints(point);
return rect.contains(point[0], point[1]);
}
}
Well, if you want drag objects in this view, you probably need to create ViewGroup and place objects as Views. Because all of your object drawn on canvas you can't tap and drag on them.
You can rotate, translate, and resize views and also intercept touch events on them to perform dragging
Your solution works only like Image with text and, if I understand you correctly, you can't do with them what you want
Related
I would like to create a brush effect, which when the user drags his/her finger on the screen, the brush only changes the alpha of the image/bitmap below. For example, if the brush opacity is set to 50%, then applying brush should
Set the alpha/opacity of that region, only that region not the whole bitmap.
Leave the underlying colors of the bitmap region unchanged.
That is, the brush should only affect the alpha transparency not the color. I am starting with the following code:
public class DrawingView extends View {
//drawing path
private Path drawPath;
//drawing and canvas paint
private Paint drawPaint, canvasPaint;
//initial color
private int paintColor = 0xFFFF0000, paintAlpha = 255;
//canvas
private Canvas drawCanvas;
//canvas bitmap
private Bitmap canvasBitmap;
//constructor
public DrawingView(Context context, AttributeSet attrs){
super(context, attrs);
setupDrawing();
}
//prepare drawing
private void setupDrawing(){
drawPath = new Path();
drawPaint = new Paint();
drawPaint.setColor(paintColor);
drawPaint.setAntiAlias(true);
drawPaint.setStrokeWidth(50);
drawPaint.setStyle(Paint.Style.STROKE);
drawPaint.setStrokeJoin(Paint.Join.ROUND);
drawPaint.setStrokeCap(Paint.Cap.ROUND);
canvasPaint = new Paint(Paint.DITHER_FLAG);
}
//view assigned size
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
drawCanvas = new Canvas(canvasBitmap);
}
//draw view
#Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
canvas.drawPath(drawPath, drawPaint);
}
//respond to touch interaction
#Override
public boolean onTouchEvent(MotionEvent event) {
float touchX = event.getX();
float touchY = event.getY();
//respond to down, move and up events
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
drawPath.moveTo(touchX, touchY);
break;
case MotionEvent.ACTION_MOVE:
drawPath.lineTo(touchX, touchY);
break;
case MotionEvent.ACTION_UP:
drawPath.lineTo(touchX, touchY);
drawCanvas.drawPath(drawPath, drawPaint);
drawPath.reset();
break;
default:
return false;
}
//redraw
invalidate();
return true;
}
//return current alpha
public int getPaintAlpha(){
return Math.round((float)paintAlpha/255*100);
}
//set alpha
public void setPaintAlpha(int newAlpha){
paintAlpha=Math.round((float)newAlpha/100*255);
drawPaint.setColor(paintColor);
drawPaint.setAlpha(paintAlpha);
}
}
As you have noticed, this brush needs to have a fill color. That is what want to avoid. I need a brush with transparency, but not color.
If there is another method to achieve the object, that is fine.
Setting an absolute alpha value is likely going to involve some pixel-twiddling in the bitmap.
The closest Porter-Duff mode for what you want is DST_OUT, which will act like an eraser.
Try this code and see if it comes close:
drawPaint.setColor(Color.BLACK); // the color doesn't really matter
drawPaint.setAlpha(255 - alpha); // inverse of the alpha result you want
drawPaint.setXferMode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
What this is going to do is make the image pixels lighter and lighter every time you draw over the same spot, which probably isn't what you want. In which case you would have to get more complicated and draw the color+alpha paths onto a separate bitmap, then draw that bitmap over your image bitmap using PorterDuff.Mode.DST_OUT.
It's unfortunate that you can't simply implement a custom ColorFilter that will do exactly what you want. All those graphics operations happen in native space and Google doesn't want us mere mortals messing with that stuff.
I'm trying to do simple painter. The problem that it looks like Android has three independent Canvas and give me it for drawing sequentially.
I made UI with SurfaceView, took Holder from it.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sv = (SurfaceView) findViewById(R.id.sv);
holder = sv.getHolder();
holder.addCallback(callback);
}
Then took Surface.
#Override
public void surfaceCreated(SurfaceHolder holder) {
surface = holder.getSurface();
}
And by events from OnTouchListener() draw dots and lines.
private void paintStartDot(float x, float y) {
Canvas canvas = surface.lockCanvas(null);
canvas.drawPoint(x, y, drawPaint);
surface.unlockCanvasAndPost(canvas);
lastX = x;
lastY = y;
}
private void paintEndDot(float x, float y) {
Canvas canvas = surface.lockCanvas(null);
canvas.drawLine(lastX, lastY, x, y, drawPaint);
surface.unlockCanvasAndPost(canvas);
lastX = x;
lastY = y;
}
The screencast:
https://youtu.be/NNDnzrtMLZI
What is wrong?
Full source is available here:
https://github.com/tseglevskiy/canvasdemo1/blob/error/app/src/main/java/ru/jollydroid/canvasdemo1/MainActivity.java
The Canvas that Surface.lockCanvas gives you is not persistent. The moment you call unlockCanvasAndPost, the contents of the surface buffer are pushed out to the screen. Every time you call lockCanvas you need to redraw the picture from scratch.
If you want to update the canvas incrementally, you should keep an "off-screen" canvas backed by a Bitmap that you update in response to user actions. Then paint the bitmap to the Surface canvas.
private void paintStartDot(float x, float y) {
if (mBitmap == null) return; // not ready yet
Canvas canvas = new Canvas(mBitmap);
canvas.drawPoint(x, y, drawPaint);
// draw the bitmap to surface
canvas = surface.lockCanvas(null);
canvas.drawBitmap(mBitmap, 0, 0, null);
surface.unlockCanvasAndPost(canvas);
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// good place to create the Bitmap
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config. ARGB_8888);
// also here we need to transform the old bitmap to new one
}
I'm not sure if you really need a SurfaceView. You could just extend the View and override its View.onDraw method. In that case, call view.invalidate() to indicate it needs to be redrawn.
See also: Android View.onDraw() always has a clean Canvas
I would like to create a generic ViewGroup which can then be reused in XML layouts to round the corners of anything that is put into it.
For some reason canvas.clipPath() doesn't seem to have an effect. What am I doing wrong?
Here is the Java code:
package rounded;
import static android.graphics.Path.Direction.CCW;
public class RoundedView extends FrameLayout {
private float radius;
private Path path = new Path();
private RectF rect = new RectF();
public RoundedView(Context context, AttributeSet attrs) {
super(context, attrs);
this.radius = attrs.getAttributeFloatValue(null, "corner_radius", 0f);
}
#Override
protected void onDraw(Canvas canvas) {
int savedState = canvas.save();
float w = getWidth();
float h = getHeight();
path.reset();
rect.set(0, 0, w, h);
path.addRoundRect(rect, radius, radius, CCW);
path.close();
boolean debug = canvas.clipPath(path);
super.onDraw(canvas);
canvas.restoreToCount(savedState);
}
}
Usage in XML:
<?xml version="1.0" encoding="utf-8"?>
<rounded.RoundedView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
corner_radius="40.0" >
<RelativeLayout
android:id="#+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent" >
...
</RelativeLayout>
</rounded.RoundedView>
The right way to create a ViewGroup that clip its children is to do it in the dispatchDraw(Canvas) method.
This is an example on how you can clip any children of a ViewGroup with a circle:
private Path path = new Path();
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// compute the path
float halfWidth = w / 2f;
float halfHeight = h / 2f;
float centerX = halfWidth;
float centerY = halfHeight;
path.reset();
path.addCircle(centerX, centerY, Math.min(halfWidth, halfHeight), Path.Direction.CW);
path.close();
}
#Override
protected void dispatchDraw(Canvas canvas) {
int save = canvas.save();
canvas.clipPath(circlePath);
super.dispatchDraw(canvas);
canvas.restoreToCount(save);
}
the dispatchDraw method is the one called to clip children. No need to setWillNotDraw(false) if your layout just clip its children.
This image is obtained with the code above, I just extended Facebook ProfilePictureView (which is a FrameLayout including a square ImageView with the facebook profile picture):
So to achieve a round border you do something like this:
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// compute the path
path.reset();
rect.set(0, 0, w, h);
path.addRoundRect(rect, radius, radius, Path.Direction.CW);
path.close();
}
#Override
protected void dispatchDraw(Canvas canvas) {
int save = canvas.save();
canvas.clipPath(path);
super.dispatchDraw(canvas);
canvas.restoreToCount(save);
}
You can actually create any complex path :)
Remember you can call clipPath multiple times with the "Op" operation you please to intersect multiple clipping in the way you like.
NOTE: I created the Path in the onSizeChanged because doing so in the onDraw is bad for performance.
NOTE2: clipping a Path is done without anti-aliasing :/ so if you want smooth borders you'll need to do it in some other way. I'm not aware of any way of making clipping use anti-aliasing right now.
UPDATE (Outline)
Since Android Lollipop (API 21) elevation and shadows can be applied to views. A new concept called Outline has been introduced. This is a path that tells the framework the shape of the view to be used to compute the shadow and other things (like ripple effects).
The default Outline of the view is a rectangular of the size of the view but can be easily made an oval/circle or a rounded rectangular. To define a custom Outline you have to use the method setOutlineProvider() on the view, if it's a custom View you may want to set it in the constructor with your custom ViewOutlineProvider defined as inner class of your custom View. You can define your own Outline provider using a Path of your choice, as long as it is a convex path (mathematical concept meaning a closed path with no recess and no holes, as an example neither a star shape nor a gear shape are convex).
You can also use the method setClipToOutline(true) to make the Outline also clip (and I think this also works with anti-aliasing, can someone confirm/refute in comments?), but this is only supported for non-Path Outline.
Good luck
You can override the draw(Canvas canvas) method:
public class RoundedLinearLayout extends LinearLayout {
Path mPath;
float mCornerRadius;
public RoundedLinearLayout(Context context) {
super(context);
}
public RoundedLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RoundedLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public void draw(Canvas canvas) {
canvas.save();
canvas.clipPath(mPath);
super.draw(canvas);
canvas.restore();
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
RectF r = new RectF(0, 0, w, h);
mPath = new Path();
mPath.addRoundRect(r, mCornerRadius, mCornerRadius, Direction.CW);
mPath.close();
}
public void setCornerRadius(int radius) {
mCornerRadius = radius;
invalidate();
}
}
onDraw of FrameLayout is not called if Ur Layout's background not set;
You should override dispatchDraw;
ViewGroup (and hence its subclasses) sets a flag indicating that it doesn't do any drawing by default. In source it looks somewhat like this:
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
So your onDraw(...) probably doesn't get hit at all right now. If you want to do any manual drawing, call setWillNotDraw(false).
Don't forget to ask for onDraw to be called with setWillNotDraw(false); and set a value to mRadius then just do something like that :
#Override
protected void onDraw(Canvas canvas) {
mPath.reset();
mRect.set(0, 0, canvas.getWidth(), canvas.getHeight());
mPath.addRoundRect(mRect, mRadius, mRadius, Direction.CCW);
mPath.close();
canvas.clipPath(mPath);
}
U need to override the drawChild() method to clip childViews.
#Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
int flag = canvas.save();
canvas.clipPath(pathToClip);
boolean result=super.drawChild(canvas, child, drawingTime);
canvas.restoreToCount(flag);
return result;
}
If u want to clip the background of the ViewGroup too,override draw() instead.like this
#Override
public void draw(Canvas canvas) {
int flag = canvas.save();
canvas.clipPath(pathToClip);
super.draw(canvas);
canvas.restoreToCount(flag);
}
Why not just define a ShapeDrawable as the background of your layout and just change the corner radius of the drawable at runtime.
In my custom views OnDraw method I draw a Bitmap to the center of the Canvas with
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Rect r = canvas.getClipBounds();
displayWidth = r.right;
displayHeight = r.bottom;
camera.applyToCanvas(canvas);
float zW = (float)bitmapWidth / (float)displayWidth;
float zH = (float)bitmapHeight / (float)displayHeight;
float z = 0.0f;
if (zW>1 || zH>1) {
z = Math.max(zW, zH);
}
canvas.drawColor(Color.DKGRAY);
canvas.drawBitmap(bitmap, (displayWidth/2.0f - (bitmapWidth)/2.0f), (displayHeight/2.0f - bitmapHeight/2.0f), paint);
if (z>0) {
camera.translate(z, -z, z);
}
}
If the Bitmap is larger in height or width is larger then Canvas size (displayWidth, displayHeight), how can I use the Camera class to autozoom to fit the Bitmap and center it to the Canvas. Any ideas?
Try to create a Matrix instance and initialize it using
public boolean setRectToRect (RectF src, RectF dst, Matrix.ScaleToFit stf)
You need to do this only once and keep the matrix in memory. Note that Matrix.ScaleToFit define the CENTER value.
Later when you draw the bitmap use this version of the drawBitmap:
public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)
I want to rotate the canvas circularly on its center axis based on user touch.
Like a old phone dialer .
i want to rotate based on center
but
Now its rotating based on top left corner .so i am able to see only 1/4 for rotation of image.
I have tried like as follows
onDraw(Canvas canvas){
canvas.save();
// do my rotation
canvas.rotate(rotation,0,0);
canvas.drawBitmap( ((BitmapDrawable)d).getBitmap(),0,0,p );
canvas.restore();
}
#Override
public boolean onTouchEvent(MotionEvent e) {
float x = e.getX();
float y = e.getY();
updateRotation(x,y);
mPreviousX = x;
mPreviousY = y;
invalidate();
}
private void updateRotation(float x, float y) {
double r = Math.atan2(x - centerX, centerY - y);
rotation = (int) Math.toDegrees(r);
}
You need to add this to your custom view methods
#Override
public void onSizeChanged (int w, int h, int oldw, int oldh){
super.onSizeChanged(w, h, oldw, oldh);
screenW = w;
screenH = h;
}
This method will give you the canvas size then use
canvas.rotate(rotation, screenW / 2, screenH / 2);
Pass the point of rotation to rotate api:
canvas.rotate(rotation, 0, centerY);
Instead of rotating the Canvas you can use
x=0;
x=x+50;
yourview.setRotation(x);
use this on touch event and
x=x-50 for rotating backword
I think it will help