4

I'm trying to implement a handle to scale a view in android. Instead of using something like multitouch I want to be able to resize an image with just one finger.

Here is my activity code. I feel as though I am very close there are a five things that don't work properly.

  1. The scaling is off. It grows at a much faster rate than it should. Solved Thanks @Salauyou
  2. The view will only grow, and not shrink. Solved Thanks @Salauyou
  3. The handle view doesn't move with the image. Solved Thanks @Salauyou
  4. The scaling starts extremely small Solved Thanks @Salauyou
  5. The handle doesn't follow your finger exactly.

I am looking for any help that could implement such a feature. Whether it's a library or someone can help with my code that I already have. I have found a library that helps with multi touch scaling of images (https://github.com/brk3/android-multitouch-controller) but the only pointer I could pick up was how to go about implementing the increase in scale. And this has to be done through using two points, and finding the distance between them.

My java activity:

public class MainActivity extends Activity {
    ImageView imageView;
    ImageView dragHandle;
    RelativeLayout layout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        imageView = (ImageView) findViewById(R.id.imageView1);
        imageView.setBackgroundColor(Color.MAGENTA);
        dragHandle = (ImageView) findViewById(R.id.imageView2);
        dragHandle.setBackgroundColor(Color.CYAN);
        layout = (RelativeLayout) findViewById(R.id.relativeLayout2);
        layout.setBackgroundColor(Color.YELLOW);
        setUpResize();

    }

    public void setUpResize() {
        dragHandle.setOnTouchListener(new View.OnTouchListener() {

            int[] touchPoint = new int[2];
            int[] centerOfImage = new int[2];

            double originalDistance = 0;
            double modifiedDistance = 0;

            float originalScale = 0;
            float modifiedScale = 0;

            public boolean onTouch(View v, MotionEvent motionEvent) {

                if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {

                    centerOfImage[0] = (int) (imageView.getX() + imageView.getWidth() / 2);
                    centerOfImage[1] = (int) (imageView.getY() + imageView.getHeight() / 2);

                    touchPoint[0] = (int) motionEvent.getRawX();
                    touchPoint[1] = (int) motionEvent.getRawY();

                    int[] p = new int[2];
                    p[0] = touchPoint[0] - centerOfImage[0];
                    p[1] = touchPoint[1] - centerOfImage[1];

                    originalDistance = (float) Math.sqrt(p[0] * p[0] + p[1] * p[1]);
                    originalScale = imageView.getScaleX();

                } else if (motionEvent.getAction() == MotionEvent.ACTION_MOVE) {
                    touchPoint[0] = (int) motionEvent.getRawX();
                    touchPoint[1] = (int) motionEvent.getRawY();

                    int[] p = new int[2];
                    p[0] = (touchPoint[0] + p[0] - centerOfImage[0]);
                    p[1] = (touchPoint[1] + p[1] - centerOfImage[1]);
                    modifiedDistance = Math.hypot(touchPoint[0] - centerOfImage[0], touchPoint[1] - centerOfImage[1]);

                    Log.e("resize", "original " + imageView.getWidth() + " modified: " + imageView.getHeight());
                    modifiedScale = (float) (modifiedDistance / originalDistance * originalScale);

                    imageView.setScaleX(modifiedScale);
                    imageView.setScaleY(modifiedScale);

                    dragHandle.setX(centerOfImage[0] + imageView.getWidth()/2 * modifiedScale);
                    dragHandle.setY(centerOfImage[1] + imageView.getHeight()/2 * modifiedScale);

                } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {

                }
                return true;
            }
        });
    }
}

My xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <RelativeLayout
        android:id="@+id/relativeLayout2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true" >

        <ImageView
            android:id="@+id/imageView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:src="@drawable/ic_launcher" />

        <ImageView
            android:id="@+id/imageView2"
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:layout_below="@+id/imageView1"
            android:layout_toRightOf="@+id/imageView1"
            android:src="@drawable/dragArrow" />

    </RelativeLayout>

</RelativeLayout>

enter image description here

  • any reason for not using http://developer.android.com/reference/android/widget/ZoomButtonsController.html ? – pskink Apr 15 '14 at 19:07
  • I don't believe having accessory zoom controls is what I need. Sorry. –  Apr 15 '14 at 19:49
  • _The handle view doesn't move with the image._ because it shouldn't. Your big image is treated for container as it is original size, because `setScaleX()` and other transformation methods effect only on view's draw, but not its room in layout. – Alex Salauyou Apr 15 '14 at 20:27
  • `Math.sqrt(p[0] * p[0] + p[1] * p[1])` could be easily replaced by `Math.hypot(p[0], p[1])` - it's more readable, precise and effective. – Alex Salauyou Apr 15 '14 at 20:29
  • So scale does not change size? @Salauyou? –  Apr 15 '14 at 20:31
  • @user2676468 scaling, rotation, skewation, translation and other matrix transformations change size in terms of _drawing_, not _layouting_. That's why your small image doesn't follow the corner of large one. – Alex Salauyou Apr 15 '14 at 20:34
  • So if I wanted to change it's size, I would have to use imageView.setWidth() imageView.setHeight()? –  Apr 15 '14 at 20:35
  • @user2676468 no, it's bad idea, just move handler image to appropriate distance using `setTranslationX()` and `setTranslationY()` – Alex Salauyou Apr 15 '14 at 20:37
  • What can I use to pass into setTranslationX,Y? Wouldn't setX(motionEvent.getRawX,Y) work better? –  Apr 15 '14 at 20:41
  • I'm not going to rewrite your code since it have much to rewrite... but can provide steps which should be followed. At `ACTION_DOWN`, remember initial coordinates tmpX and tmpY of touch event, using `motionEvent.getRawX()` and `.getRawY()`; and calculate initial `tmpR`. At `ACTION_MOVE` obtain new x and y and calculate new R, based on finger position relative to anchor point of big image, then scale big image and translate small one based on these values. – Alex Salauyou Apr 15 '14 at 20:44
  • no problem, use `setX()` if you want. I mean that it should be done explicitly – Alex Salauyou Apr 15 '14 at 20:46
  • And I still don't understand the purpose of the statement `(imageView.getScaleX() * R / tmpR) * 0.1`. What does `*0.1` stand for? – Alex Salauyou Apr 15 '14 at 20:49
  • `setX(motionEvent.getRawX,Y)` this will make small image follow the finger, but I think it should follow the right bottom corner of big image, no? – Alex Salauyou Apr 15 '14 at 20:51
  • @Salauyou I want the small image to follow the finger, and the bottom right corner of the image. Just like in photoshop. The onTouchListener is only set on the dragHandle. –  Apr 15 '14 at 20:53
  • @user2676468 I was sure you wanted to avoid unproportional scaling using a kind of "radius" (R and tmpR) and applying `scaleX` and `scaleX` with the same value, no? – Alex Salauyou Apr 15 '14 at 20:57
  • @Salauyou ALso, *0.1 doesn't really mean anything. I saw some code snippet online that used 0.2 so I was just playing around with values. –  Apr 15 '14 at 20:57
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/50727/discussion-between-salauyou-and-user2676468) – Alex Salauyou Apr 15 '14 at 20:57

1 Answers1

2

So... The main problem is that getRawX() and getRawY() methods of MotionEvent provide absolute screen coordinates, while getX() and getY() provide layout coordinates. Coordinates thus differ on heights of progress bar and status bar, so when obtaining touch coordinates, we should recalculate them relatively to layout.

dragHandle.setOnTouchListener(new View.OnTouchListener() {

    float centerX, centerY, startR, startScale, startX, startY;

    public boolean onTouch(View v, MotionEvent e) {

        if (e.getAction() == MotionEvent.ACTION_DOWN) {

            // calculate center of image
            centerX = (imageView.getLeft() + imageView.getRight()) / 2f;
            centerY = (imageView.getTop() + imageView.getBottom()) / 2f;

            // recalculate coordinates of starting point
            startX = e.getRawX() - dragHandle.getX() + centerX;
            startY = e.getRawY() - dragHandle.getY() + centerY; 

            // get starting distance and scale
            startR = (float) Math.hypot(e.getRawX() - startX, e.getRawY() - startY);
            startScale = imageView.getScaleX();

        } else if (e.getAction() == MotionEvent.ACTION_MOVE) {

            // calculate new distance
            float newR = (float) Math.hypot(e.getRawX() - startX, e.getRawY() - startY);

            // set new scale
            float newScale = newR / startR * startScale;
            imageView.setScaleX(newScale);
            imageView.setScaleY(newScale);

            // move handler image
            dragHandle.setX(centerX + imageView.getWidth()/2f * newScale);
            dragHandle.setY(centerY + imageView.getHeight()/2f * newScale);

        } else if (e.getAction() == MotionEvent.ACTION_UP) {

        }
        return true;
    }
});

Also, I replaced hypothenuse calculation by library method and declared all coordinates as float to avoid unnecessary casting.

Alex Salauyou
  • 14,185
  • 5
  • 45
  • 67
  • This works great. I need to now rotate and scale, but I'm getting a problem when combining the two. http://stackoverflow.com/questions/23115472/rotate-and-scale-a-view-based-on-one-handle-in-android –  Apr 16 '14 at 16:50
  • you want to perform 2 operations sumultaneously by dragging one handler? – Alex Salauyou Apr 16 '14 at 16:52
  • Yes. I once saw an app on iOS that does it. And it works very well, but I do not remember the name. I want it to work like pinch to zoom works. If you pinch to zoom, you can also rotate at the same time. So yes, 2 operations at once. –  Apr 16 '14 at 16:57
  • So you want to pinch-zoom/rotate (which uses 2 fingers) or drag-zoom/rotate (which uses 1 finger and handler icon)? Approaches and solutions will be different. – Alex Salauyou Apr 16 '14 at 17:05