I have a HorizontalScrollView in which there are multiple views inside it. I have implemented pinch zoom gesture in which multiple views, which are between my two fingers, are scaled.
But I am facing one glitch. When I am doing pinch zoom, the mid point of pinch zoom is moving but for user experience I want this point to be remain fixed and other things should adjust itself while scaling so that mid point remains static.
Can someone tell me how to do it.
onDraw() method of custom view is
Rect r =new Rect(0, 0, 700, 40);
public void onDraw(Canvas canvas) {
float kk=mScaleFactor; //this variable will be set by pinch zoom event and represent how much to scale
sf*=kk; // this view must scale according to previous scaling
canvas.save();
canvas.scale(sf , 1);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.BLUE);
canvas.drawRect(r, paint);
width=sf*700;
canvas.restore();
requestLayout(); //this will change view width to fit the expanded rectangle
}
onMeasure method is called on requestLayout
#Override protected void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
setMeasuredDimension((int)width, 340);
}
The above custom view is called 12 times by a subclass of HorizontalScrollView. So there are 12 child views of HorizontalScrollview.
In this class I am doing the following things
Detecting the touch coordinates of two fingers.
Calculating the index of child view on which first finger is touched.
Calculating the index of child view on which second finger is touched.
Passing scale factor of pinch zoom to all the child views between start and last.
These two indices are calculated in previous two step.
And finally invalidate() is called on the child view. So child can scale itself according to scale factor passed by parent view.
But there is one problem here. The mid point of two finger should remain static and other things should adjust during scaling. But my mid point is moving with scaling.
Can someone help me in doing this. Please tell me if any part of code is require.
Code of gesture listener of HorizontalScrollview is
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
mScaleFactor *= detector.getScaleFactor();
mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));
ViewGroup pp=takeparent(); //give object of this class
for(int i=start;i<=last;i++)
{
LinearLayout ll=(LinearLayout)pp.getChildAt(i);
DrawView view=(DrawView)ll.findViewById(R.id.drawview);
view.mScaleFactor=mScaleFactor;
view.invalidate();
view.donesf=true;
}
Sample app of mine
Edits as suggested in comments:
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
mScaleFactor *= detector.getScaleFactor();
mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));
ViewGroup pp=takeparent(); pp contains all the custom child views
//start and last are indices of range of child for which we have to apply gesture
for(int i=start;i<=last;i++)
{
LinearLayout ll=(LinearLayout)pp.getChildAt(i);
DrawView view=(DrawView)ll.findViewById(R.id.drawview);
view.mScaleFactor=mScaleFactor;
view.pivotx=detector.getFocusX()+view.getLeft();
view.pivoty=detector.getFocusY()+view.getTop();
view.invalidate();
}
return true;
}
This is custom view onDraw method
public void onDraw(Canvas canvas) {
sf=mScaleFactor //this scale factor is set by parent view
width=sf*700; //this width will be use to rescale the view's width in requestLayout()
canvas.save();
canvas.scale(sf,1,pivotx,pivoty); //pivotX and pivotY are also set by parent's onScale method of gestureListener
canvas.drawRect(r, paint);
requestLayout();
canvas.restore();
}
For a view you are scaling, you can set the "invariant" point that should not move using setPivotX() and setPivotY(). When I use a ScaleGestureDetector, I use the focus point for this. For example:
#Override
public boolean onScaleBegin(ScaleGestureDetector detector)
{
...
mScaledView.setPivotX(detector.getFocusX());
mScaledView.setPivotY(detector.getFocusY());
...
I'm not sure if you want to do this with all of the children, or just the parent view, in your case, but usually you just need to do this to a single "parent" view and it will work properly for the children of that view.
Related to this, I didn't quite understand why you're passing the scaling factor down to each child when scaling the parent view will scale all of its children too. Maybe you just need to add a single FrameLayout (or some other ViewGroup descendent) into your HorizontalScrollView that hosts the children and then just scale that (after setting its pivot appropriately)?
Updated re comment
Given that you only want to scale the views in the pinched region between the fingers, I believe you have a couple options, depending on your app:
1) Dynamically add just those views to an intermediate FrameLayout which gets scaled and has its pivot set, leaving the non-scaled views as direct children of the HorizontalScrollView; or
2) Passing down the focus point to each scaled child view after first adjusting for the child's position. For example, assuming your DrawView either directly or indirectly inherits from android.view.View, which I would strongly recommend, then you can do something like:
for (int i = start; i <= last; i++) {
LinearLayout ll = (LinearLayout)pp.getChildAt(i);
DrawView view = (DrawView)ll.findViewById(R.id.drawview);
view.setScaleX(mScaleFactor);
view.setScaleY(mScaleFactor);
view.setPivotX(detector.getFocusX() - view.getLeft()); // Note: minus, not plus...
view.setPivotY(detector.getFocusY() - view.getTop());
view.invalidate();
}
Related
I was surprised to find that View.onDraw() wipes the canvas before drawing. This isn't what I want. What's a good way to retain the previous drawings so that I only need to draw the changes on top of the previous drawings during each call?
There are couple of APIs to define dirty rect for view to invalidate:
public void invalidate(Rect dirty)
public void invalidate(int l, int t, int r, int b)
However more likely the View is redrawn all the way, when for example you swipe it, or another View above it in Z order get's invalidated.
You can try to use setDrawingCacheEnabled and if you are doing the drawing yourself, be sure to cache Bitmaps you are drawing.
I implemented a SurfaceView. Then in I created my own bitmap and canvas that I draw into, to draw to the screen I draw the bitmap to the screen.
For example:
private Canvas myCanvas = null;
private Bitmap myCanvasBitmap = null;
private Matrix identityMatrix;
public mySurfaceCreated(canvasWidth, canvasHeight){
myCanvasBitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
myCanvas = new Canvas();
myCanvas.setBitmap(myCanvasBitmap);
identityMatrix = new Matrix();
}
public void myDraw(Canvas canvas) {
// Draw stuff onto myCanvas, NOT the canvas given by the android OS.
drawStuff(myCanvas);
// Draw onto the canvas given by the android OS.
canvas.drawBitmap(myCanvasBitmap, identityMatrix, null);
}
This way I do not need to draw the entire canvas each time, but instead only the needed changes. Note that the canvas created by the OS is still completely redrawn each time.
I've been having a lot of issues drawing multiple paths in the same relative layout. What happens is that all my paths are drawn on the same spot they were originally drawn. Instead I want to shrink each drawn path/canvas and have them displayed side by side on the page.
My code to draw the paths looks like
for (int x=0; x < paths.size(); x++){
DrawView dw = new CustomView(this);
dw.path = paths.get(x);
dw.paint = paints.get(x);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
if (x == 0){
} else {
params.addRule(RelativeLayout.BELOW, x-1);
}
dw.setId(x);
layout.addView(dw, params);
}
I followed other suggestions for TextViews to add an custom layout parameter to display each TextView below each other but this does not appear to work for dynamically drawn paths.
Note: CustomView is a class that extends View and overwrites the onDraw method to draw out my paths.
EDIT:
If it helps my custom class looks like
public class CustomView extends View {
public Path path;
public Paint paint = new Paint();
public CustomView(Context context){
super(context);
paint.setAntiAlias(true);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
}
#Override
protected void onDraw(Canvas canvas){
canvas.drawPath(path, paint);
}
}
Is it necessary for you to use a RelativeLayout? If it is not too binding, consider having a LinearLayout which either replaces the RelativeLayout, or is contained within the RelativeLayout. Then you can add all the CustomViews to this LinearLayout, setting the weight field in LinearLayout.LayoutParams to 1.
Alternatively, consider manually setting each CustomView's height to getHeight()/(paths.size()) instead of RelativeLayout.LayoutParams.WRAP_CONTENT.
I am trying set up a kids app where the can draw on a canvas.
I have a layout which has an ImageView and looks something like this:
The Green background with the letter is the imageview, where the canvas should overlay and allow user to draw on. Also I want to allow the user to change the color stroke by pressing on the color globe.
How can I go on with it?
This answer here might help you with allowing the user the canvas: https://stackoverflow.com/a/7401699/2698582.
To allow the user to draw in different colors, you can use a color picker such as this one: https://code.google.com/p/android-color-picker/, or you can google for a different one.
If you define a new point class and change a bit of this example code, you'll be able to add this color change:
private class ColoredPoint {
public int x, y, color = Color.BLACK;
}
public class DrawView extends View implements OnTouchListener {
List<ColoredPoint> points = new ArrayList<ColoredPoint>();
Paint paint = new Paint();
int currentColor = Color.BLACK;
public DrawView(Context context) {
super(context);
setFocusable(true);
setFocusableInTouchMode(true);
this.setOnTouchListener(this);
paint.setColor(currentColor);
}
#Override
public void onDraw(Canvas canvas) {
for (ColoredPoint point : points) {
paint.setColor(point.color);
canvas.drawCircle(point.x, point.y, 2, paint);
}
}
public boolean onTouch(View view, MotionEvent event) {
ColoredPoint point = new ColoredPoint();
point.color = currentColor;
point.x = event.getX();
point.y = event.getY();
points.add(point);
invalidate();
return true;
}
}
Finally, depending on your selected color picker, this code will vary. Basically, you'll add a touch listener to your color globe to show the color popup. Supposing that the popup has an "OK" button, you'll add a button listener to that button. When it is pressed, change the variable "currentColor" to the selected color.
As mentioned in that example post, you can also implement this using Lines. You might consider using a GestureDetector instead. This tutorial should help explain exactly how the GestureDetector works: http://developer.android.com/training/gestures/detector.html.
Take linearlayout and transfer imageview into linearlayout background color. Add your custom view into this linearlayout, and override ondraw method to that view class.
something like this:
<LinearLayout
background:#drawable/foo">
<com.packagename.customview
android:width="match_parent"
android:height="value">
>
</com.packagename.customview>
</LinearLayout>
create customview class and implement drawing part into it.
For the color picker, just use this link: https://code.google.com/p/android-color-picker/
I have a canvas / surfaceview that I am drawing multiple bitmaps so that the user is able to drag around and position anywhere on the canvas. I am looking for a method to create a zoom in and out button that toggles a 50% zoom scale of the canvas when selected.
My app draws multiple bitmaps to the canvas using the onDraw() method.
I have a function now that scales each individual bitmap when the zoom button is toggled but the problem with this is getting the bitmaps to line up relative to each other after the scale. I currently have them scaled off their ending x/y position but by reducing the size 50% the items that were once near each other are no longer that way when zoomed out. I am hoping there is a solution that allows me to accomplish this with the entire canvas at once.
If I need to share some code please let me know.
SurfaceView class where all the drawing happens
public class BuildView extends SurfaceView implements SurfaceHolder.Callback {
then non-scaling drawing happens as canvas.drawBitmap(myBitmap,x,y,null);
Here is my oncreate method, note that BuildView gameView; is initating my surfaceview class;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set full screen view
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
requestWindowFeature(Window.FEATURE_NO_TITLE);
FrameLayout Game = new FrameLayout(this);
LinearLayout GameWidgets = new LinearLayout (this);
gameview = new BuildView(this, null);
//create buttons to lay over the surfaceview
Button AddItemButton = new Button(this);
//Button ZoomInButton = new Button(this);
zoomButton = new Button(this);
TextView MyText = new TextView(this);
AddItemButton.setWidth(250);
AddItemButton.setText("Add Item");
AddItemButton.setId(1);
AddItemButton.setOnClickListener(addItemHandler);
MyText.setText("test");
zoomButton.setWidth(250);
zoomButton.setText("Zoom Out");
zoomButton.setId(2);
zoomButton.setOnClickListener(zoomHandler);
// adding views to the main Game framelayout view * note the view "gameview" is the surface view that everything happens on
GameWidgets.addView(AddItemButton);
GameWidgets.addView(zoomButton);
Game.addView(gameview);
Game.addView(GameWidgets);
int w=getWindowManager().getDefaultDisplay().getWidth()-25;
int h=getWindowManager().getDefaultDisplay().getHeight()-25;
gameview.wid = w; //set width and height in view
gameview.hei = h;
gameview.setWidthHeight(w, h);
setContentView(Game); //trying this out
gameview.updateImage(0, 0); //force bg change
Log.i("views", "Added Game View");
//setContentView(v);
}
since it has been requested here is how I am currently scaling the bitmaps, note that I am looking for another method of scaling the entire canvas. Here is the specifics of my onDraw() method but the full method has more logic in regards to item placement and before scaling I check a boolean flag if the user has selected to scale but i figured you wouldn't want to sift through that much code so I am just putting the pertinent stuff here.
protected void onDraw(Canvas canvas) {
float scaleHeight = getScaled(myBitmap.getHeight());
float scaleWidth = getScaled(myBitmap.getWidth());
scaler.postTranslate(-myBitmap.getWidth() / 4, -itemBit.getHeight() / 4); // Centers image
scaler.postScale(scaleWidth, scaleHeight);
Log.e("onDraw", "item not placed - scaler.postTranslate being called");
scaler.postTranslate(itemX+myBitmap.getWidth()/2, itemY+myBitmap.getHeight()/2);
canvas.drawBitmap(myBitmap, scaler, null);
}
here is my getScaled function;
float getScaled(int orig) {
//return a scaled version
int newScale = orig /2;
float scaled = ((float) newScale) / orig;
return scaled;
}
I need some help understanding the fundamentals of scrolling over items that are drawn to a canvas in Android. Suppose I want to create a timeline where time at 0 is the top of a visualization and as time increased the timeline continues to be rendered below the previous point. If I wish to render this on Android I know I could simply create a bunch of items on a canvas by overriding onDraw(). However, let's suppose the visualization is bigger than the screen allows.
For example in the first picture below the large black box contains the entire canvas as I render it. I create a blue line that runs vertically up and down as well as several yellow, green and blue rectangles. The red box represents the screen of the Android that is rendering the visualization. As it initially opens all items are drawn but only the items contained within the red box show up on the screen.
Now if the user is to scroll down, items that initially appeared below the red box are in view while items that have gone out of the confines of the red box are no longer visable, as represented in the second picture.
I believe I need to use scrollables but I'm quite lost how to do so. I've read over this page http://developer.android.com/training/custom-views/custom-drawing.html explaining how to create your own customer images and this page http://developer.android.com/training/custom-views/making-interactive.html explaining how to make the UI interactive, but I think I'm missing something.
A sample code that illustrates this problem (this is basic, assume there is logic dictating WHERE the boxes/lines go, etc.) is as follows:
package com.example.scrolltest;
import com.example.scrolltest.Draw;
import android.os.Bundle;
import android.app.Activity;
import android.graphics.Color;
public class MainActivity extends Activity {
Draw draw;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
draw = new Draw(this);
draw.setBackgroundColor(Color.WHITE);
setContentView(draw);
}
}
and
package com.example.scrolltest;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;
public class Draw extends View {
Paint paint = new Paint();
public Draw(Context context) {
super(context);
}
#Override
public void onDraw(Canvas canvas) {
paint.setColor(Color.GREEN);
canvas.drawRect(30, 30, 90, 200, paint);
paint.setColor(Color.BLUE);
canvas.drawLine(100, 20, 100, 1900, paint);
paint.setColor(Color.GREEN);
canvas.drawRect(200, 2000, 400, 3000, paint);
}
}
What I cannot figure out though, is how I use a scrollable to scroll down to the rectangles that are off of the screen. I'm also unsure if I started this correctly or should use drawables instead...
Simple Method (If the height required is not very large).
Use a ScrollView and add your Draw view in it. Compute the required height for that view in onMeasure.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
draw = new Draw(this);
draw.setBackgroundColor(Color.WHITE);
ScrollView scrollView = new ScrollView(this);
scrollView.addView(draw);
setContentView(scrollView);
}
public class Draw extends View {
Paint paint = new Paint();
public Draw(Context context) {
super(context);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Compute the height required to render the view
// Assume Width will always be MATCH_PARENT.
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = 3000 + 50; // Since 3000 is bottom of last Rect to be drawn added and 50 for padding.
setMeasuredDimension(width, height);
}
#Override
public void onDraw(Canvas canvas) {
paint.setColor(Color.GREEN);
canvas.drawRect(30, 30, 90, 200, paint);
paint.setColor(Color.BLUE);
canvas.drawLine(100, 20, 100, 1900, paint);
paint.setColor(Color.GREEN);
canvas.drawRect(200, 2000, 400, 3000, paint);
}
}
An alternative can be to use offset X/Y values.
How you handle the offset values is up to you, all though I prefer using a class I call Camera. The access should be static.
public void render(Canvas c){
c.drawRect(100 - offsetX, 100 - offsetY, 120 - offsetX, 120 - offsetY, Paints.BLUE);
}
And to pan the canvas:
float x, y;
#Override
public boolean onTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN){
x = ev.getX();
y = ev.getY();
}else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
float currX = ev.getX();
float currY = ev.getY();
float newOffsetX = (x - currX),
newOffsetY = (y - currY);
//The next two if-statements are to add sensitivity to the scrolling.
//You can drop these if you don't plan on having touch events
//with pressing. Pressing works without this as well, but the canvas may move slightly
//I use DP to handle different screen sizes but it can be replaced with pixels. c is a context-instance
if (newOffsetY < Maths.convertDpToPixel(2, c) && newOffsetY > -Maths.convertDpToPixel(2, c))
newOffsetY = 0;
if (newOffsetX < Maths.convertDpToPixel(2, c) && newOffsetX > -Maths.convertDpToPixel(2, c))
newOffsetX = 0;
offsetX += newOffsetX;
offsetY += newOffsetY;
x = ev.getX();
y = ev.getY();
}
return true;
}
And a sample Camera-class:
public class Camera{
public static float offsetX, offsetY;
//The constructor. ix and iy is the initial offset. Useful if you are creating a game and need to change the initial offset to center around a starting position.
//Most of the time it will be enough to set the values to 0
public Camera(float ix, float iy){
this.offsetX = ix;
this.offsetY = iy;
}
}