Search code examples
androidandroid-custom-viewviewgroupandroid-viewgroup

How to reposition imageview when scaling custom viewgroup?


Goal

I am currently working on a small project that effectively does to two things:

  • Shows a scalable/dragable image by using a custom ViewGroup (This works well)
  • Shows 1-N markers/pins on the image, using coordinates (x/y pixels) within the image as anchor points. This should not scale along with the ViewGroup, only reposition itself. (This is not working well)

Problem (this is what I can't figure out!)

What I cannot make work, is positioning of the marker while scaling. I have managed to place the marker at the right place on the device's screen (in the image of the ViewGroup) when launching the Activity, but when I start scaling the ViewGroup, the marker seem to be using the same x/y relative coordinates as the ViewGroup itself. This means that the marker will move longer off its "pinned" place the more I scale the view.

More information

The layout component tree, since that may be a possible source of grief, I'm currently using is:

  • RelativeLayout
    • CustomViewGroup (scalable/dragable)
      • ImageView
    • ImageView (holding a single marker for now)

I'm using two matrices; one for the ViewGroup and one for the marker. I think I need two matrices so that the marker can move independently of the ViewGroup (even if it would follow a lot of the ViewGroup's patterns).

Code in the ViewGroup

// Used to initially position the marker on the screen
public void placeMarker(float x, float y) {
    RelativeLayout parent = (RelativeLayout) this.getParent();
    mMapMarker = (ImageView) parent.findViewById(R.id.map_marker);

    // Set initial placement of marker (redundant if this can be fixed in dispatchDraw)
    mMapMarker.setX(x);
    mMapMarker.setY(y);

    // Setting the marker's matrix to the same initial values as the placement for use when the view translates/scales 
    mMatrixMarker.postTranslate(x, y);
    mMatrixMarker.postScale(1f,1f,mid.x,mid.y);
    mMatrixMarker.invert(mMatrixInverseMarker);
}

@Override
protected void dispatchDraw(Canvas canvas) {
    // Draw the marker using ImageView.setX/Y
    float[] markerValues = new float[9];
    mMatrixMarker.getValues(markerValues);
    mMapMarker.setX(markerValues[Matrix.MTRANS_X]);
    mMapMarker.setY(markerValues[Matrix.MTRANS_Y]);

    float[] values = new float[9];
    matrix.getValues(values);
    canvas.save();
    // Translate the canvas for panning and scaling the view
    canvas.translate(values[Matrix.MTRANS_X], values[Matrix.MTRANS_Y]);
    canvas.scale(values[Matrix.MSCALE_X], values[Matrix.MSCALE_Y]);
    super.dispatchDraw(canvas);
    canvas.restore();
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    ...
    } else if (mode == ZOOM) {
      if (event.getPointerCount() > 1) {
          float newDist = spacing(event);
            if (newDist > 10f * density) {
                matrix.set(savedMatrix);
                float scale = (newDist / oldDist);
                float[] values = new float[9]; matrix.getValues(values);
                if(scale*values[Matrix.MSCALE_X] >= MAX_ZOOM) {
                    scale = MAX_ZOOM/values[Matrix.MSCALE_X];
                }
                if(scale*values[Matrix.MSCALE_X] <= MIN_ZOOM) {
                    scale = MIN_ZOOM/values[Matrix.MSCALE_X];
                }
                // Save the change in scale to the ViewGroup's matrix
                matrix.postScale(scale, scale, mid.x, mid.y);
                matrix.invert(matrixInverse);

                // Save the same change in scale for the marker (redundant if the scale is not supposed to be different)
                mMatrixMarker.set(mSavedMatrixMarker);
                mMatrixMarker.postScale(scale, scale, mid.x, mid.y);
                mMatrixMarker.invert(mMatrixInverseMarker);
            }
        } else {
            // Save the change in scale to the ViewGroup's matrix
            matrix.set(savedMatrix);
            float scale = event.getY() / start.y;
            matrix.postScale(scale, scale, mid.x, mid.y);
            matrix.invert(matrixInverse);

            // Save the same change in scale for the marker (redundant if the scale is not supposed to be different)
            mMatrixMarker.set(mSavedMatrixMarker);
            mMatrixMarker.postScale(scale, scale, mid.x, mid.y);
            mMatrixMarker.invert(mMatrixInverseMarker);
        }
    }
    ...
}

----EDIT #1----

I have now made changes to the code, in which I am now using what I believe is a more proper matrix implementation, thanks to pskinks tip. I see how my code has improved by it and I appreciate that.

However, what happens now is that I'm in a state where the marker is placed properly in the ViewGroup and either of the following things happen:

  • The marker scales along with the ViewGroup scaling. This makes the marker stay in the correct "pinned" place but also makes it shrink/grow with the ViewGroup, which I'm trying to avoid.
  • The marker does not scale with the ViewGroup scaling. This makes the marker fixed and misaligning the more the ViewGroup scales. The fixed marker size is correct, but I need a way to "counteract" the offset during scaling.

New code in the ViewGroup

// Used to initially position the marker on the screen
public void placeMarker(float x, float y) {
    RelativeLayout parent = (RelativeLayout) this.getParent();
    mMapMarker = (ImageView) parent.findViewById(R.id.map_marker);
    mMapMarker.setScaleType(ImageView.ScaleType.MATRIX);

    // Setting the marker's matrix to the same initial values as the placement for use when the view translates/scales
    mMatrixMarker.postTranslate(x, y);
    mMatrixMarker.postScale(1f,1f,mid.x,mid.y);
    mMatrixMarker.invert(mMatrixInverseMarker);
}

@Override
protected void dispatchDraw(Canvas canvas) {

    // Draw the marker using a Matrix
    mMapMarker.setImageMatrix(mMatrixMarker);

    canvas.save();
    // Translate the canvas for panning and scaling the view
    canvas.translate(values[Matrix.MTRANS_X], values[Matrix.MTRANS_Y]);
    canvas.scale(values[Matrix.MSCALE_X], values[Matrix.MSCALE_Y]);
    super.dispatchDraw(canvas);
    canvas.restore();
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    ...
    } else if (mode == ZOOM) {
      if (event.getPointerCount() > 1) {
          float newDist = spacing(event);
            if (newDist > 10f * density) {
                matrix.set(savedMatrix);
                float scale = (newDist / oldDist);
                float[] values = new float[9]; matrix.getValues(values);
                if(scale*values[Matrix.MSCALE_X] >= MAX_ZOOM) {
                    scale = MAX_ZOOM/values[Matrix.MSCALE_X];
                }
                if(scale*values[Matrix.MSCALE_X] <= MIN_ZOOM) {
                    scale = MIN_ZOOM/values[Matrix.MSCALE_X];
                }
                // Save the change in scale to the ViewGroup's matrix
                matrix.postScale(scale, scale, mid.x, mid.y);
                matrix.invert(matrixInverse);

                // If this is included, the marker shrink/grows with the scaling of the ViewGroup
                // If this is not included, the marker stays in a fixed position, making it become misaligned during scaling
                mMatrixMarker.set(mSavedMatrixMarker);
                mMatrixMarker.postScale(scale, scale, mid.x, mid.y);
                mMatrixMarker.invert(mMatrixInverseMarker);
            }
        } else {
            // Save the change in scale to the ViewGroup's matrix
            matrix.set(savedMatrix);
            float scale = event.getY() / start.y;
            matrix.postScale(scale, scale, mid.x, mid.y);
            matrix.invert(matrixInverse);

            // If this is included, the marker shrink/grows with the scaling of the ViewGroup
            // If this is not included, the marker stays in a fixed position, making it become misaligned during scaling
            mMatrixMarker.set(mSavedMatrixMarker);
            mMatrixMarker.postScale(scale, scale, mid.x, mid.y);
            mMatrixMarker.invert(mMatrixInverseMarker);
        }
    }
    ...
}

Solution

  • I managed to figure this out, and it came down to keeping track of the marker's initial position and scale and reuse those values to calculate its new position when dragging and scaling.

    The marker's initial position code

    public void placeMarker(float x, float y) {
        RelativeLayout parent = (RelativeLayout) this.getParent();
        mMapMarker = (ImageView) parent.findViewById(R.id.map_marker);
        mMapMarker.setScaleType(ImageView.ScaleType.MATRIX);
    
        mMatrixMarker.postTranslate(x, y);
        mMatrixMarker.postScale(1f,1f,mid.x,mid.y);
        mMatrixMarker.invert(mMatrixInverseMarker);
    
        mMatrixMarkerSource.postTranslate(x,y);
        mMatrixMarkerSource.postScale(1f,1f,mid.x,mid.y);
        mMatrixMarkerSource.invert(mMatrixInverseMarker);
        float[] valuesMatrixSource = new float[9];
        mMatrixMarkerSource.getValues(valuesMatrixSource);
    }
    

    The usage of the marker's initial position

    /*
    Draw the requested changes taking pan and zoom into account
    */
    @Override
    protected void dispatchDraw(Canvas canvas) {
        // Keep a reference to the marker's initial location and scale for comparison
        float[] valuesMarkerSource = new float[9];
        mMatrixMarkerSource.getValues(valuesMarkerSource);
        float sx = valuesMarkerSource[Matrix.MTRANS_X];
        float sy = valuesMarkerSource[Matrix.MTRANS_Y];
    
        // Keep a reference to the current location and scale of out ViewGroup
        float[] values = new float[9];
        matrix.getValues(values);
    
        Drawable d = getResources().getDrawable(R.drawable.ic_map_marker_150);
        // Adjust marker image position based on its measured size
        float vX = ((sx * values[Matrix.MSCALE_X]) + values[Matrix.MTRANS_X]) - (d.getIntrinsicWidth()/2);
        float vY = ((sy * values[Matrix.MSCALE_Y]) + values[Matrix.MTRANS_Y]) - (d.getIntrinsicHeight());
        mMatrixMarker.reset();
        mMatrixMarker.postTranslate(vX, vY);
    
    
        float[] valuesMarker = new float[9];
        mMatrixMarker.getValues(valuesMarker);
    if(mMapMarker != null) {
      // We want to use a marker, so save the translated matrix to it
      mMapMarker.setImageMatrix(mMatrixMarker);
    } else {
      /*
      TODO This is suboptimal. We really should just hide the marker once and not deal with it any more
    
      We don't want to use a marker, so hide it
       */
      RelativeLayout parent = (RelativeLayout) this.getParent();
      mMapMarker = (ImageView) parent.findViewById(R.id.map_marker);
      mMapMarker.setVisibility(ImageView.INVISIBLE);
    }
    

    I also uploaded the whole project to GotHub: https://github.com/shellstrom/ffa