NinjaLama
NinjaLama

Reputation: 43

Pan/Expand view/canvas in android

I've got this canvas which the user can add images/text etc to. If the user drags one of the items to either side of the canvas, it should expand if needed. I googled, but couldn't find any reasonable solution. Also, the canvas is about 90% of the screen width, and 70% of the height.. I'm not asking for an entire solution.. I just need a tip on how to do this (Links, docs, whatever)

Upvotes: 2

Views: 2800

Answers (1)

Simon
Simon

Reputation: 14472

Well, it's difficult to guess what you're trying to achieve. When you say "it should expand if needed", what do you mean? Expand to fill the parent view? Expand to it's intrinsic size?

Here's some (incomplete) code I use in a custom view class. Most of it is gleaned from multiple solutions on here and I give thanks to the original authors. The onDraw is the most interesting one. When you want to draw (where it says custom drawing here), you don't need to worry about translation or scaling as the canvas itself is translated and scaled. In other words, your x and y co-ords are relative to the view size - simply multiply them by scale.

public class LightsViewer extends ImageView {

private float scale;

// minimum and maximum zoom
private float MIN_ZOOM = 1f;
private float MAX_ZOOM = 5f;

// mid point between fingers to centre scale
private PointF mid = new PointF();

private float scaleFactor = 1.f;
private ScaleGestureDetector detector;

// drag/zoom mode
private final static int NONE = 0;

// current mode
private int mode ;

private float startX = 0f;
private float startY = 0f;

private float translateX = 0f;
private float translateY = 0f;

private float previousTranslateX = 0f;
private float previousTranslateY = 0f;

public LightsViewer(Context context) {

    super(context);
    detector = new ScaleGestureDetector(getContext(), new ScaleListener());

    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Display display = wm.getDefaultDisplay();

    displayWidth = display.getWidth();
    displayHeight = display.getHeight();

}


public LightsViewer(Context context, AttributeSet attrs) {

    super(context,attrs);
    detector = new ScaleGestureDetector(getContext(), new ScaleListener());

    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Display display = wm.getDefaultDisplay();

    displayWidth = display.getWidth();
    displayHeight = display.getHeight();

}

@Override
public boolean onTouchEvent(MotionEvent event) {

    int ZOOM = 2;
    int DRAG = 1;

    if (!allowZooming){return true;}

    switch (event.getAction() & MotionEvent.ACTION_MASK) {

        case MotionEvent.ACTION_DOWN:

            mode = DRAG;

            //We assign the current X and Y coordinate of the finger to startX and startY minus the previously translated
            //amount for each coordinates This works even when we are translating the first time because the initial
            //values for these two variables is zero.
            startX = event.getX() - previousTranslateX;
            startY = event.getY() - previousTranslateY;

            break;

        case MotionEvent.ACTION_MOVE:

            if (mode == ZOOM){
                Log.d("LIGHTS","ACTION_MOVE:Move but ZOOM, breaking");
                break;}

            translateX = event.getX() - startX;
            translateY = event.getY() - startY;

            //We cannot use startX and startY directly because we have adjusted their values using the previous translation values.
            //This is why we need to add those values to startX and startY so that we can get the actual coordinates of the finger.
            double distance = Math.sqrt(Math.pow(event.getX() - (startX + previousTranslateX), 2) +
                    Math.pow(event.getY() - (startY + previousTranslateY), 2)
            );

            if(distance > 0) {
                dragged = true;
            }

            break;

        case MotionEvent.ACTION_POINTER_DOWN:

            midPoint(mid, event);

            mode = ZOOM;

            break;

        case MotionEvent.ACTION_UP:

            mode = NONE;
            dragged = false;

            //All fingers went up, so let's save the value of translateX and translateY into previousTranslateX and
            //previousTranslate
            previousTranslateX = translateX;
            previousTranslateY = translateY;

            break;

        case MotionEvent.ACTION_POINTER_UP:

            mode = NONE;

            //This is not strictly necessary; we save the value of translateX and translateY into previousTranslateX
            //and previousTranslateY when the second finger goes up
            previousTranslateX = translateX;
            previousTranslateY = translateY;

            break;
    }

    detector.onTouchEvent(event);

    //We redraw the canvas only in the following cases:
    //
    // o The mode is ZOOM
    //        OR
    // o The mode is DRAG and the scale factor is not equal to 1 (meaning we have zoomed) and dragged is
    //   set to true (meaning the finger has actually moved)
    if ((mode == DRAG && scaleFactor != 1f && dragged) || mode == ZOOM) {            
        this.invalidate();
    }

    return true;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if (this.getImageMatrix().isIdentity()){return;}

    if (allowZooming){
        this.applyMatrix(this.getImageMatrix());
    }

}

@Override
public void onDraw(Canvas canvas) {

    canvas.save();

    // scale the canvas
    canvas.scale(scaleFactor, scaleFactor, mid.x, mid.y);

    canvas.translate(translateX / scaleFactor, translateY / scaleFactor);

    super.onDraw(canvas);

    // do custom drawing here...e.g.
    canvas.drawCircle(100,100, 3 / scaleFactor,light.paint);


    canvas.restore();
}

private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        scaleFactor *= detector.getScaleFactor();
        scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
        return true;
    }
}

// calculate the mid point of the first two fingers
private void midPoint(PointF point, MotionEvent event) {
    // ...
    float x = event.getX(0) + event.getX(1);
    float y = event.getY(0) + event.getY(1);
    point.set(x / 2, y / 2);

    Log.d("LIGHTS", x/2 + "," + y/2);

}

public void setMinZoom(float minZoom){
    this.MIN_ZOOM = minZoom;
}

public void applyMatrix(Matrix matrix){

    float[] matrixValues = new float[9];
    matrix.getValues(matrixValues);

    int x = (int)matrixValues[Matrix.MTRANS_X];
    int y = (int)matrixValues[Matrix.MTRANS_Y];
    float scale = matrixValues[Matrix.MSCALE_X];

    if (lights!=null){
        for (Light light:lights){
            light.setX((int)((light.originalX * scale) + x));
            light.setY((int)((light.originalY * scale) + y));
        }
    }

    // if either the x or y translations are less than 0, then the image has been cropped
    // so set the min zoom to the ratio of the displayed size and the actual size of the image
    if (matrixValues[Matrix.MTRANS_X] < 0 || matrixValues[Matrix.MTRANS_Y] <0){
        MIN_ZOOM = displayWidth / this.getDrawable().getIntrinsicWidth();
    }else{
        MIN_ZOOM = 1;
    }

}

public void enableZooming(boolean enable){
    allowZooming = enable;
}

public void setScale(float scale){

    for (Light light:lights){
        light.setX((int)(light.originalX * scale));
        light.setY((int)(light.originalY * scale));
    }
}

}

Upvotes: 2

Related Questions