209

Is there a common way to show a big image and enable the user to zoom in and out and pan the image?

Until now I found two ways:

  1. overwriting ImageView, that seems a little bit too much for such a common problem.
  2. using a webview but with less control over the overall layout etc.
Vega
  • 27,856
  • 27
  • 95
  • 103
Janusz
  • 187,060
  • 113
  • 301
  • 369
  • There is a ZOOM CONTROL (Widget) and you can listen to the OnTouch event to handle the panning. – tobrien Mar 29 '10 at 13:33
  • 1
    A similar question http://stackoverflow.com/questions/2537396/full-size-image-in-imageview, has a link to this tutorial http://www.anddev.org/large_image_scrolling_using_low_level_touch_events-t11182.html. You might find that useful to pan your iamge. I haven't read it in detail, but it might also give you some ideas on how to do the zoom function as well. – Steve Haley Mar 29 '10 at 13:45
  • Have anyone tried to save the image when zooming? I want the saved image on a default state instead of the zoomed state. Please see my question : http://stackoverflow.com/questions/24730793/save-original-image-png-even-when-zoomed Thanks – Blaze Tama Jul 18 '14 at 05:00

13 Answers13

209

UPDATE

I've just given TouchImageView a new update. It now includes Double Tap Zoom and Fling in addition to Panning and Pinch Zoom. The code below is very dated. You can check out the github project to get the latest code.

USAGE

Place TouchImageView.java in your project. It can then be used the same as ImageView. Example:

TouchImageView img = (TouchImageView) findViewById(R.id.img);

If you are using TouchImageView in xml, then you must provide the full package name, because it is a custom view. Example:

<com.example.touch.TouchImageView
    android:id="@+id/img”
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Note: I've removed my prior answer, which included some very old code and now link straight to the most updated code on github.

ViewPager

If you are interested in putting TouchImageView in a ViewPager, refer to this answer.

Community
  • 1
  • 1
Mike Ortiz
  • 4,031
  • 4
  • 27
  • 54
  • @Tsunaze Copy this file into your project and use it basically the same as ImageView. I edited the post to add usage details. You can also check out the code on Github and look into TouchImageViewActivity.java for more help, but the code I provided above is exactly how it's used. – Mike Ortiz Sep 20 '11 at 23:36
  • Hi Mike, your solution seems much more elegant then the normal one, but at least here on my Xoom, the zooming is very slow, compared to the original code. Do you see that too? – Paulo Cesar Oct 03 '11 at 14:52
  • 4
    Paulo, I did not run into performance issues, but I didn't get to test on a tablet. By slow, do you mean laggy? I set a max zoom factor of 1.05 at the beginning of onScale. Is this what you're talking about? If not, try the following: 1. Are you in debug mode? This would slow it down significantly. 2. What size images are you setting. I didn't test with very large (8mp) images, but this might slow it down. 3. Do you have a phone you could test on? 4. If all else fails, see if multiplying mScaleFactor by 2 (if > 1) or 0.5 (if < 1) helps your situation. – Mike Ortiz Oct 04 '11 at 00:51
  • Just to follow up, the issue seems to be in the limits set on mScaleFactor. These limits are too small for the tablet. A quick solution is to get rid of the max and min and change the line to `float mScaleFactor = detector.getScaleFactor();`. – Mike Ortiz Oct 04 '11 at 23:39
  • @MikeOrtiz : thanks for your solution. However, if I try to use `TouchImageView` in the layout file, I get the following error : `Error inflating class TouchImageView` ..any help ? – Ahsan Oct 26 '11 at 14:21
  • 3
    @Ahsan Change the View constructor to: `TouchImageView(Context context, AttributeSet attrs)` and call `super(context, attrs);` This is because when you inflate the custom view, it is constructed with two parameters, rather than just one. When I get around to it, I will fix TouchImageView to support the three view constructors and drawables. – Mike Ortiz Oct 26 '11 at 19:54
  • @MikeOrtiz Sorry, didnt work out. Is there something else that I am missing ? – Ahsan Nov 01 '11 at 23:43
  • 2
    @Ahsan Because it is a custom view, you need to write out the whole name in the XML file, ie ``. Did you do that? – Mike Ortiz Nov 02 '11 at 05:56
  • @MikeOrtiz : Thanks got it working.. looks like an error on my behalf...extremely sorry :( – Ahsan Nov 05 '11 at 16:44
  • @MikeOrtiz : Also, is there a way to programmatically zoom the image in this way ? – Ahsan Nov 05 '11 at 22:39
  • @MikeOrtiz i have these class and used for my project it's working. but when i am using through xml i am not able to getting these things please help me. – RajaReddy PolamReddy Dec 07 '11 at 06:15
  • @Raja Look at the previous two responses to Ahsan. You must change the View constructor and call the view by it's full name, because it is a custom view. I have not updated the code, because I don't have access to a device at the moment and I don't want to update with untested code. I will, however, make a note of it in the description. – Mike Ortiz Dec 07 '11 at 08:06
  • @MikeOrtiz i followed those instruction, i am not getting an error but i am unalbe to do zoom an psnning. in java TouchImageView mWebView = (TouchImageView) findViewById(R.id.image); mWebView.setImageBitmap(originalBitmap); mWebView.setMaxZoom(4f); – RajaReddy PolamReddy Dec 07 '11 at 08:55
  • Sorry, I'm not sure what the issue is then. This is the first complaint I've heard like this. I assume you are testing on a device, right? It probably won't work on an emulator. Also make sure your project is for 2.2 or above. Some debugging tips: put Logcat print statements in the MotionEvent switch statement to make sure touch events are registering correctly. In onScale, make sure mScaleFactor is varying. The scaling stuff is mostly solid, so my hunch is that there's an issue with your view registering touch events. Please post an update here when you figure it out. – Mike Ortiz Dec 07 '11 at 10:40
  • Also, is this TouchImageView the only view occupying the screen? I did not test for multiple views in one screen or know of anyone who has used my code that way. – Mike Ortiz Dec 07 '11 at 10:42
  • @MikeOrtiz thank you, nice solution for panning and zooming. my problem solved i tried with in layout through that i cat't get this.. now i am using TouchImageView only in total screen. – RajaReddy PolamReddy Dec 07 '11 at 12:17
  • It's not working for me in a Fragment. Nothing is rendered, either when configured via XML (with constructor changes) or adding dynamically. :-( public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.rx_info_type1, container, false); ViewGroup content = (ViewGroup)view.findViewById(R.id.content);TouchImageView image = new TouchImageView(getActivity());image.setImageResource(R.drawable.esc2010);image.setMaxZoom(4f);content.addView(image);return view; } – Ollie C Dec 27 '11 at 17:37
  • The problem was setImageResource() - it didn't work for me. I had to load the bitmap from resource into a Bitmap object, and use setImageBitmap() instead. – Ollie C Dec 27 '11 at 18:07
  • @MikeOrtiz I have just tried your code and it works pretty well. But I have noticed that it's not possible to move the image around when it's zoomed out. Any idea how to change this (using your code)? – Amokrane Chentir Jan 06 '12 at 16:43
  • @MikeOrtiz Can u pls help me? I need to set buttons on the touchImageView. Is there a way to resize/move the button within the touchimageview so the buttons are always on the same location on the image.. (Like map annotation on map, but this time on a image) see: http://stackoverflow.com/questions/9767294/how-to-place-buttons-on-a-zoompan-imageview – Luciano Mar 19 '12 at 15:39
  • Hi @MikeOrtiz It works fine,Only problem that I am facing is onTap action. When on tap action is performed I wants setVisible for layout.I am using as XML – sandy Apr 02 '12 at 13:04
  • Hi @Mike Ortiz, I tried your code, it works but it isn't smooth and not elastic like Gallery app. Can you teach me fix it for smooth. – emeraldhieu May 14 '12 at 07:56
  • Have just tesed. It doesn't work in Fragment, image is displayed but it cannot be zoomed. – emeraldhieu May 14 '12 at 08:09
  • 1
    This is great stuff, been looking for this for ages. Do use the code from github as it is more recent and just works better – Alex Jul 16 '12 at 18:20
  • @MikeOrtiz Great custom ImageView class. One issue I have found: I have the TouchImageView as a child in a RelativeLayout, unfortunately it seems, android:layout_height="wrap_content" cuts off the bottom of the image when it is loaded. Of course when I use "match_parent" it works fine by filling the height of the screen, however, I have other views that need to also be on the screen without overlaying the image. Anyway, I took a look at the Source for ImageView, [link](http://example.com) particularly the onMeasure function. And made some changes to yours. See answer below. – digiphd Jul 23 '12 at 08:24
  • But if my image is in gallery then it could not swipe the image in gallery ..... so how to detect swipe effect in order to swipe image in gallery. i.e. i want zooming effect and swipe effect for image in gallery simultaneously............. – Umesh Oct 03 '12 at 13:37
  • @Umesh TouchImageView does not currently support usage in a gallery. [Here is a link](https://github.com/allholy1/TouchImageView) to someone who forked my code so that it could work in a gallery. I haven't personally tested it yet, so let us know if it works! – Mike Ortiz Oct 03 '12 at 20:36
  • 2
    @Mike i have tried this code but custom gallery doesn't work. Is there any trick work around this problem. – Umesh Oct 04 '12 at 06:36
  • This code seems to somehow get over the issue "Bitmap too large to be uploaded into a texture". I tried the github code with 12MP (4000+ pixels) bitmap and it just works! But once I use the same library in my own app, and "Bitmap too large..." error is back. Where is the trick? I've been googleing for days. – A.P. Apr 23 '13 at 15:36
  • @MikeOrtiz Will this support double tap zoom-in?? Because I could only get the imageview to zoom-in using pinch-out. And that too doesn't work well while pinching-out horizontally. vertically it works fine. – Archie.bpgc May 06 '13 at 06:45
  • @MikeOrtiz how to reset zoom and translation.. I mean original one when display first time. I tried with resetting matrix but failed to get. Can you? – Bhavesh Hirpara May 09 '13 at 10:18
  • @sunshine Check out this [answer](http://stackoverflow.com/questions/13918488/how-to-properly-set-fixtransx-fixtransy-of-touchimageview-to-implement-doublet/14027274#14027274), which adapts TouchImageView to implement double tap zoom (ie resetting zoom and translation). – Mike Ortiz May 09 '13 at 16:44
  • @MikeOrtiz I had already found your answer today. You got upvote today :D. Thanks for reply. – Bhavesh Hirpara May 09 '13 at 17:18
  • @Mike, any suggestions on making your code available for drawables as well? I already added public void setImageDrawable (Drawable drawable) to the code setting bmWidth and bmHeight, the drawable is shown, but the zooming seems to be very slow. Ideas what else I have to adjust? Thanks! – Tobi N Jun 27 '13 at 14:19
  • @MikeOrtiz: Thanks for the code. I've made several improvements, including making it compatible with API level 4 (Android 1.6) and adding long touch detection. My code is available [on GitHub](https://github.com/adamantoise/wordswithcrosses/blob/master/wordswithcrosses/src/com/adamrosenfield/wordswithcrosses/view/TouchImageView.java). – Adam Rosenfield Jul 06 '13 at 20:44
  • @TobiN I'd recommend checking out the code on github. It is more up to date with support for drawables. Also, are you using a tablet? The above code placed a limitation on zoom speed which was very noticeable on tablets, but the code on github should've fixed this issue. – Mike Ortiz Jul 08 '13 at 00:07
  • @AdamRosenfield Thanks Adam, I will check it out. – Mike Ortiz Jul 08 '13 at 00:08
  • Very outdated. No pinching to zoom. – Johann Jul 26 '13 at 09:33
  • @AndroidDev Pinch zoom is supported. Check the github link for the most updated code. Are you having issues with pinch zoom when using the github code? – Mike Ortiz Jul 26 '13 at 20:16
  • OK, the version I got didn't have it. But with your updated version it is clear that pinch zooming only works when the device is held in portrait mode. If you turn the device to landscape mode and pinch vertically, it won't zoom. But if you pinch horizontally it will. Pinching should zoom in or out smoothly regardless what the orientation of the device is. – Johann Jul 27 '13 at 03:09
  • @AndroidDev That's unexpected as I use ScaleGestureDetector to detect pinch zoom. I haven't run into this issue so it could be phone specific. What kind of device are you using? – Mike Ortiz Jul 29 '13 at 17:15
  • Huawei Y200 and Huawei Y300. I doubt it has to do with the device. People mostly take photos in landscape mode, so pinching in landscape mode to zoom is actually the norm. – Johann Jul 30 '13 at 03:41
  • @MikeOrtiz: Hi Mike! I used your pinch-zoom library (TouchImageView.java). Amazing work! But, i'm facing a little problem with your Github code. I want my imageview to fit the screen. "android:scaleType:fitxy" in the layout xml was taking care of that before i had integrated pinch-zoom functionality. Now that I'm using in my layout instead of normal , scaleType:fitxy probably gets overridden by scaleType.MATRIX in your TouchImageView.java code. I changed it to FIT_XY (in your code) but pinch-zoom stopped working. Please help . – epiphany27 Sep 23 '13 at 08:00
  • @MikeOrtiz I found that when the screen rotates, the image doesn't get resized maintaining aspect ratio. it has blank area on the top, that's why it creates problem positioning the image. Probably have to fix onMeasure method. https://github.com/MikeOrtiz/TouchImageView/issues/40 – StarDust Nov 16 '13 at 04:07
  • @MikeOrtiz this works beautifully! However now I'm struggling to make doubletap to zoom in to maximum scale and centered on the double tap point.. help! – Bruce Nov 30 '13 at 01:23
  • @Bruce You're in luck. I've just implemented this feature. You can check it out on github! – Mike Ortiz Dec 06 '13 at 05:34
  • 1
    @StarDust Thank you for pointing this out. This problem has been fixed in the latest code on github. – Mike Ortiz Dec 06 '13 at 05:36
  • @MikeOrtiz The double tap has introduced another BUG. If you double tap on an ImageView to zoom it, then set another picture on that ImageView dynamically, the images doesn't show up. – StarDust Dec 10 '13 at 23:27
  • @MikeOrtiz I created an issue on your git repo: https://github.com/MikeOrtiz/TouchImageView/issues/41 – StarDust Dec 10 '13 at 23:38
  • 1
    @StarDust Thanks, I've noticed this bug as well and I am working on it. I responded to the git issue. – Mike Ortiz Dec 11 '13 at 00:50
  • might seem stupid question but where are your constants defined? NONE, FLING etc EDIT: Oh, they are an ENUM. I actually had to go through and prepend all of them with `State.` ie, `State.NONE` – wired00 Feb 05 '14 at 15:59
  • Unfortunately, this lib does not seem to work properly in scrollview [Related issue...](https://github.com/MikeOrtiz/TouchImageView/issues/157) – erginduran Apr 02 '17 at 13:03
  • @Mike how can I set fixed size for imageview? So if I pinch in/out, image size will not change. – Erselan Khan Jun 24 '19 at 17:16
80

I adapted some code to create a TouchImageView that supports multitouch (>2.1). It is inspired by the book Hello, Android! (3rd edition)

It is contained within the following 3 files TouchImageView.java WrapMotionEvent.java EclairMotionEvent.java

TouchImageView.java

import se.robertfoss.ChanImageBrowser.Viewer;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.util.FloatMath;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;

public class TouchImageView extends ImageView {

    private static final String TAG = "Touch";
    // These matrices will be used to move and zoom image
    Matrix matrix = new Matrix();
    Matrix savedMatrix = new Matrix();

    // We can be in one of these 3 states
    static final int NONE = 0;
    static final int DRAG = 1;
    static final int ZOOM = 2;
    int mode = NONE;

    // Remember some things for zooming
    PointF start = new PointF();
    PointF mid = new PointF();
    float oldDist = 1f;

    Context context;


    public TouchImageView(Context context) {
        super(context);
        super.setClickable(true);
        this.context = context;

        matrix.setTranslate(1f, 1f);
        setImageMatrix(matrix);
        setScaleType(ScaleType.MATRIX);

        setOnTouchListener(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent rawEvent) {
                WrapMotionEvent event = WrapMotionEvent.wrap(rawEvent);

                // Dump touch event to log
                if (Viewer.isDebug == true){
                    dumpEvent(event);
                }

                // Handle touch events here...
                switch (event.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    savedMatrix.set(matrix);
                    start.set(event.getX(), event.getY());
                    Log.d(TAG, "mode=DRAG");
                    mode = DRAG;
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    oldDist = spacing(event);
                    Log.d(TAG, "oldDist=" + oldDist);
                    if (oldDist > 10f) {
                        savedMatrix.set(matrix);
                        midPoint(mid, event);
                        mode = ZOOM;
                        Log.d(TAG, "mode=ZOOM");
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    int xDiff = (int) Math.abs(event.getX() - start.x);
                    int yDiff = (int) Math.abs(event.getY() - start.y);
                    if (xDiff < 8 && yDiff < 8){
                        performClick();
                    }
                case MotionEvent.ACTION_POINTER_UP:
                    mode = NONE;
                    Log.d(TAG, "mode=NONE");
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (mode == DRAG) {
                        // ...
                        matrix.set(savedMatrix);
                        matrix.postTranslate(event.getX() - start.x, event.getY() - start.y);
                    } else if (mode == ZOOM) {
                        float newDist = spacing(event);
                        Log.d(TAG, "newDist=" + newDist);
                        if (newDist > 10f) {
                            matrix.set(savedMatrix);
                            float scale = newDist / oldDist;
                            matrix.postScale(scale, scale, mid.x, mid.y);
                        }
                    }
                    break;
                }

                setImageMatrix(matrix);
                return true; // indicate event was handled
            }

        });
    }


    public void setImage(Bitmap bm, int displayWidth, int displayHeight) { 
        super.setImageBitmap(bm);

        //Fit to screen.
        float scale;
        if ((displayHeight / bm.getHeight()) >= (displayWidth / bm.getWidth())){
            scale =  (float)displayWidth / (float)bm.getWidth();
        } else {
            scale = (float)displayHeight / (float)bm.getHeight();
        }

        savedMatrix.set(matrix);
        matrix.set(savedMatrix);
        matrix.postScale(scale, scale, mid.x, mid.y);
        setImageMatrix(matrix);


        // Center the image
        float redundantYSpace = (float)displayHeight - (scale * (float)bm.getHeight()) ;
        float redundantXSpace = (float)displayWidth - (scale * (float)bm.getWidth());

        redundantYSpace /= (float)2;
        redundantXSpace /= (float)2;


        savedMatrix.set(matrix);
        matrix.set(savedMatrix);
        matrix.postTranslate(redundantXSpace, redundantYSpace);
        setImageMatrix(matrix);
    }


    /** Show an event in the LogCat view, for debugging */
    private void dumpEvent(WrapMotionEvent event) {
        // ...
        String names[] = { "DOWN", "UP", "MOVE", "CANCEL", "OUTSIDE",
            "POINTER_DOWN", "POINTER_UP", "7?", "8?", "9?" };
        StringBuilder sb = new StringBuilder();
        int action = event.getAction();
        int actionCode = action & MotionEvent.ACTION_MASK;
        sb.append("event ACTION_").append(names[actionCode]);
        if (actionCode == MotionEvent.ACTION_POINTER_DOWN
                || actionCode == MotionEvent.ACTION_POINTER_UP) {
            sb.append("(pid ").append(
                    action >> MotionEvent.ACTION_POINTER_ID_SHIFT);
            sb.append(")");
        }
        sb.append("[");
        for (int i = 0; i < event.getPointerCount(); i++) {
            sb.append("#").append(i);
            sb.append("(pid ").append(event.getPointerId(i));
            sb.append(")=").append((int) event.getX(i));
            sb.append(",").append((int) event.getY(i));
            if (i + 1 < event.getPointerCount())
            sb.append(";");
        }
        sb.append("]");
        Log.d(TAG, sb.toString());
    }

    /** Determine the space between the first two fingers */
    private float spacing(WrapMotionEvent event) {
        // ...
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return FloatMath.sqrt(x * x + y * y);
    }

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

WrapMotionEvent.java

import android.view.MotionEvent;

public class WrapMotionEvent {
protected MotionEvent event;




    protected WrapMotionEvent(MotionEvent event) {
        this.event = event;
    }

    static public WrapMotionEvent wrap(MotionEvent event) {
            try {
                return new EclairMotionEvent(event);
            } catch (VerifyError e) {
                return new WrapMotionEvent(event);
            }
    }



    public int getAction() {
            return event.getAction();
    }

    public float getX() {
            return event.getX();
    }

    public float getX(int pointerIndex) {
            verifyPointerIndex(pointerIndex);
            return getX();
    }

    public float getY() {
            return event.getY();
    }

    public float getY(int pointerIndex) {
            verifyPointerIndex(pointerIndex);
            return getY();
    }

    public int getPointerCount() {
            return 1;
    }

    public int getPointerId(int pointerIndex) {
            verifyPointerIndex(pointerIndex);
            return 0;
    }

    private void verifyPointerIndex(int pointerIndex) {
            if (pointerIndex > 0) {
                throw new IllegalArgumentException(
                    "Invalid pointer index for Donut/Cupcake");
            }
    }

}

EclairMotionEvent.java

import android.view.MotionEvent;

public class EclairMotionEvent extends WrapMotionEvent {

    protected EclairMotionEvent(MotionEvent event) {
            super(event);
    }

    public float getX(int pointerIndex) {
            return event.getX(pointerIndex);
    }

    public float getY(int pointerIndex) {
            return event.getY(pointerIndex);
    }

    public int getPointerCount() {
            return event.getPointerCount();
    }

    public int getPointerId(int pointerIndex) {
            return event.getPointerId(pointerIndex);
    }
}
Abhishek Agarwal
  • 1,907
  • 12
  • 21
Robert Foss
  • 2,497
  • 4
  • 19
  • 18
  • Robert Foss,if this add boundary judge ,it can fell more well.thank you your code very well – pengwang Dec 02 '10 at 07:20
  • 3
    It works, but I don't see the point in `WrapMotionEvent` and `EclairMotionEvent`... anyway, +1. – Cipi Jun 03 '11 at 14:27
  • 2
    Multitouch for phones that support it. An regular touch for Android <2.0 – Robert Foss Jul 08 '11 at 10:04
  • Nice example it work fine but i did not get what is Viewer in if (Viewer.isDebug == true){ dumpEvent(event); } – Tofeeq Ahmad Apr 06 '12 at 06:09
  • 2
    What is this? >> se.robertfoss.ChanImageBrowser.Viewer – emeraldhieu May 14 '12 at 06:51
  • to get it to work via xml... add the following code: public TouchImageView(Context context, AttributeSet attrs) { super(context, attrs); initAll(context); } public TouchImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initAll(context); } public TouchImageView(Context context) { super(context); initAll(context); } private void initAll(Context context) { super.setClickable(true); this.context = context; ... //other init stuff from original contructor} – cV2 Aug 27 '12 at 10:41
  • It works like a charm even if I use SurfaceView! Thank you. +1 – Nolesh Oct 11 '12 at 01:22
  • @RobertFoss How do I restrict moving/panning only to bounds of view, something like Gallery app does? Currently I can move the image to the extreme sides of the view and so view will not show almost no image. – cgr Apr 24 '17 at 16:44
  • I don't know :F I haven't done any android development since writing this answer pretty much. – Robert Foss Apr 24 '17 at 21:35
60

I used a WebView and loaded the image from the memory via

webview.loadUrl("file://...")

The WebView handles all the panning zooming and scrolling. If you use wrap_content the webview won't be bigger then the image and no white areas are shown. The WebView is the better ImageView ;)

Janusz
  • 187,060
  • 113
  • 301
  • 369
  • 5
    I'm using the same approach. I have a large subway map that I want the user to be able to zoom and scroll around. I noticed though that if you have a quite large image (i.e. 1000 or 3000 pixels wide), the image gets blurry once you zoom in. It seems coliris cannot display a large zoomed image very sharp. Even though the original image is uncompressed and very sharp. Therefore I ended up cutting the one large image into smaller slices and putting them together again via HTML. This way the image stays sharp when zooming in. (I'm on Nexus One, 2.1update before and now on 2.2) – Mathias Conradt Jun 15 '10 at 02:58
  • @Mathias Lin: if a large image is sent over the wire, i've heard **carriers** compress large images. will this use-case suit you or did you load the image locally. – Samuel Feb 22 '11 at 04:42
  • @Sam Quest: loading it locally – Mathias Conradt Feb 22 '11 at 08:15
  • Mathias, I am very interested by your tiles approach. Would you agree to share some pieces of code? Seem really interesting. – Waza_Be Jul 12 '11 at 20:09
  • 4
    much better to use webview's built in zoom buttons and support for pinch to zoom in/out than to write a completely new algo which may not work across different phones and future android platform releases – sami Aug 01 '11 at 05:52
  • I tiled a large image like Mathias suggested, but when the resulting page is 3000 pixels wide, the browser refuses to zoom out far enough to see the whole page. This has me reconsidering the TouchImageView approach. – Jacob Marble Oct 28 '11 at 05:28
  • I apply your solution but white background is still showing.. Also I given background @null then also its showing................. – RobinHood Feb 03 '12 at 06:43
  • 2
    this solution can only be applied if you happen to have the image sitting around on disk, or the image is small enough such that you can base 64 encode is and pass the string value into loadUrlWithData(). – Jeffrey Blattman Sep 10 '12 at 20:10
7

In Response to Janusz original question, there are several ways to achieve this all of which vary in their difficulty level and have been stated below. Using a web view is good, but it is very limited in terms of look and feel and controllability. If you are drawing a bitmap from a canvas, the most versatile solutions that have been proposed seems to be MikeOrtiz's, Robert Foss's and/or what Jacob Nordfalk suggested. There is a great example for incorporating the android-multitouch-controller by PaulBourke, and is great for having the multi-touch support and alltypes of custom views.

Personally, if you are simply drawing a canvas to a bitmap and then displaying it inside and ImageView and want to be able to zoom into and move around using multi touch, I find MikeOrtiz's solution as the easiest. However, for my purposes the code from the Git that he has provided seems to only work when his TouchImageView custom ImageView class is the only child or provide the layout params as:

android:layout_height="match_parent"
android:layout_height="match_parent"

Unfortunately due to my layout design, I needed "wrap_content" for "layout_height". When I changed it to this the image was cropped at the bottom and I couldn't scroll or zoom to the cropped region. So I took a look at the Source for ImageView just to see how Android implemented "onMeasure" and changed MikeOrtiz's to suit.

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

  //**** ADDED THIS ********/////
      int  w = (int) bmWidth;
      int  h = (int) bmHeight;
     width = resolveSize(w, widthMeasureSpec);  
     height = resolveSize(h, heightMeasureSpec);
  //**** END ********///   

   // width = MeasureSpec.getSize(widthMeasureSpec);   // REMOVED
   // height = MeasureSpec.getSize(heightMeasureSpec); // REMOVED

    //Fit to screen.
    float scale;
    float scaleX =  (float)width / (float)bmWidth;
    float scaleY = (float)height / (float)bmHeight;

    scale = Math.min(scaleX, scaleY);
    matrix.setScale(scale, scale);
    setImageMatrix(matrix);
    saveScale = 1f;

    // Center the image
    redundantYSpace = (float)height - (scale * (float)bmHeight) ;
    redundantXSpace = (float)width - (scale * (float)bmWidth);
    redundantYSpace /= (float)2;
    redundantXSpace /= (float)2;

    matrix.postTranslate(redundantXSpace, redundantYSpace);

    origWidth = width - 2 * redundantXSpace;
    origHeight = height - 2 * redundantYSpace;
   // origHeight = bmHeight;
    right = width * saveScale - width - (2 * redundantXSpace * saveScale);
    bottom = height * saveScale - height - (2 * redundantYSpace * saveScale);

    setImageMatrix(matrix);
}

Here resolveSize(int,int) is a "Utility to reconcile a desired size with constraints imposed by a MeasureSpec, where :

Parameters:

 - size How big the view wants to be
 - MeasureSpec Constraints imposed by the parent

Returns:

 - The size this view should be."

So essentially providing a behaviour a little more similar to the original ImageView class when the image is loaded. Some more changes could be made to support a greater variety of screens which modify the aspect ratio. But for now I Hope this helps. Thanks to MikeOrtiz for his original code, great work.

digiphd
  • 2,319
  • 3
  • 23
  • 22
6

I just integrated Robert Foss's TouchImageView: it worked perfectly out of the box! Thanks!

I just modified a bit the code so I could be able to instantiate it from my layout.xml.

Just add two constructors

public TouchImageView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
}

public TouchImageView(Context context) {
    super(context);
    init(context);
}

and transform the old constructor into an init method:

private void init(Context context){
    //...old code ofconstructor of Robert Moss's code
}
zontar
  • 485
  • 7
  • 18
6

You could also try out http://code.google.com/p/android-multitouch-controller/

The library is really great, although initially a little hard to grasp.

Jacob Nordfalk
  • 3,533
  • 1
  • 21
  • 21
3

@Robert Foss, @Mike Ortiz, thank you very much for your work. I merged your work, and completed Robert classes for android > 2.0 with Mike additional work.

As result of my work I present Android Touch Gallery, based on ViewPager and used modificated TouchImageView. Images loading by URL and you can zoom and drag them. You can find it here https://github.com/Dreddik/AndroidTouchGallery

Roman Truba
  • 4,401
  • 3
  • 35
  • 60
2

Try using ZoomView for zooming any other view.

http://code.google.com/p/android-zoom-view/ it's easy, free and fun to use!

karooolek
  • 675
  • 6
  • 2
2

Adding to @Mike's answer. I also needed double tap to restore the image to the original dimensions when first viewed. So I added a whole heap of "orig..." instance variables and added the SimpleOnGestureListener which did the trick.

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.ImageView;

public class TouchImageView extends ImageView {

    Matrix matrix = new Matrix();

    // We can be in one of these 3 states
    static final int NONE = 0;
    static final int DRAG = 1;
    static final int ZOOM = 2;
    int mode = NONE;

    // Remember some things for zooming
    PointF last = new PointF();
    PointF start = new PointF();
    float minScale = 1f;
    float maxScale = 3f;
    float[] m;

    float redundantXSpace, redundantYSpace, origRedundantXSpace, origRedundantYSpace;;

    float width, height;
    static final int CLICK = 3;
    static final float SAVE_SCALE = 1f;
    float saveScale = SAVE_SCALE;

    float right, bottom, origWidth, origHeight, bmWidth, bmHeight, origScale, origBottom,origRight;

    ScaleGestureDetector mScaleDetector;
    GestureDetector mGestureDetector;

    Context context;

    public TouchImageView(Context context) {
        super(context);
        super.setClickable(true);
        this.context = context;
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());

        matrix.setTranslate(1f, 1f);
        m = new float[9];
        setImageMatrix(matrix);
        setScaleType(ScaleType.MATRIX);

        setOnTouchListener(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {

                boolean onDoubleTapEvent = mGestureDetector.onTouchEvent(event);
                if (onDoubleTapEvent) {
                    // Reset Image to original scale values
                    mode = NONE;
                    bottom = origBottom;
                    right = origRight;
                    last = new PointF();
                    start = new PointF();
                    m = new float[9];
                    saveScale = SAVE_SCALE;
                    matrix = new Matrix();
                    matrix.setScale(origScale, origScale);
                    matrix.postTranslate(origRedundantXSpace, origRedundantYSpace);
                    setImageMatrix(matrix);
                    invalidate();
                    return true;
                } 


                mScaleDetector.onTouchEvent(event);

                matrix.getValues(m);
                float x = m[Matrix.MTRANS_X];
                float y = m[Matrix.MTRANS_Y];
                PointF curr = new PointF(event.getX(), event.getY());

                switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    last.set(event.getX(), event.getY());
                    start.set(last);
                    mode = DRAG;
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (mode == DRAG) {
                        float deltaX = curr.x - last.x;
                        float deltaY = curr.y - last.y;
                        float scaleWidth = Math.round(origWidth * saveScale);
                        float scaleHeight = Math.round(origHeight * saveScale);
                        if (scaleWidth < width) {
                            deltaX = 0;
                            if (y + deltaY > 0)
                                deltaY = -y;
                            else if (y + deltaY < -bottom)
                                deltaY = -(y + bottom);
                        } else if (scaleHeight < height) {
                            deltaY = 0;
                            if (x + deltaX > 0)
                                deltaX = -x;
                            else if (x + deltaX < -right)
                                deltaX = -(x + right);
                        } else {
                            if (x + deltaX > 0)
                                deltaX = -x;
                            else if (x + deltaX < -right)
                                deltaX = -(x + right);

                            if (y + deltaY > 0)
                                deltaY = -y;
                            else if (y + deltaY < -bottom)
                                deltaY = -(y + bottom);
                        }
                        matrix.postTranslate(deltaX, deltaY);
                        last.set(curr.x, curr.y);
                    }
                    break;

                case MotionEvent.ACTION_UP:
                    mode = NONE;
                    int xDiff = (int) Math.abs(curr.x - start.x);
                    int yDiff = (int) Math.abs(curr.y - start.y);
                    if (xDiff < CLICK && yDiff < CLICK)
                        performClick();
                    break;

                case MotionEvent.ACTION_POINTER_UP:
                    mode = NONE;
                    break;
                }

                setImageMatrix(matrix);
                invalidate();

                return true; // indicate event was handled
            }

        });

        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onDoubleTapEvent(MotionEvent e) {
                return true;
            }
        });
    }

    @Override
    public void setImageBitmap(Bitmap bm) {
        super.setImageBitmap(bm);
        bmWidth = bm.getWidth();
        bmHeight = bm.getHeight();
    }

    public void setMaxZoom(float x) {
        maxScale = x;
    }

    private class ScaleListener extends
            ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            mode = ZOOM;
            return true;
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            float mScaleFactor = (float) Math.min(
                    Math.max(.95f, detector.getScaleFactor()), 1.05);
            float origScale = saveScale;
            saveScale *= mScaleFactor;
            if (saveScale > maxScale) {
                saveScale = maxScale;
                mScaleFactor = maxScale / origScale;
            } else if (saveScale < minScale) {
                saveScale = minScale;
                mScaleFactor = minScale / origScale;
            }
            right = width * saveScale - width
                    - (2 * redundantXSpace * saveScale);
            bottom = height * saveScale - height
                    - (2 * redundantYSpace * saveScale);
            if (origWidth * saveScale <= width
                    || origHeight * saveScale <= height) {
                matrix.postScale(mScaleFactor, mScaleFactor, width / 2,
                        height / 2);
                if (mScaleFactor < 1) {
                    matrix.getValues(m);
                    float x = m[Matrix.MTRANS_X];
                    float y = m[Matrix.MTRANS_Y];
                    if (mScaleFactor < 1) {
                        if (Math.round(origWidth * saveScale) < width) {
                            if (y < -bottom)
                                matrix.postTranslate(0, -(y + bottom));
                            else if (y > 0)
                                matrix.postTranslate(0, -y);
                        } else {
                            if (x < -right)
                                matrix.postTranslate(-(x + right), 0);
                            else if (x > 0)
                                matrix.postTranslate(-x, 0);
                        }
                    }
                }
            } else {
                matrix.postScale(mScaleFactor, mScaleFactor,
                        detector.getFocusX(), detector.getFocusY());
                matrix.getValues(m);
                float x = m[Matrix.MTRANS_X];
                float y = m[Matrix.MTRANS_Y];
                if (mScaleFactor < 1) {
                    if (x < -right)
                        matrix.postTranslate(-(x + right), 0);
                    else if (x > 0)
                        matrix.postTranslate(-x, 0);
                    if (y < -bottom)
                        matrix.postTranslate(0, -(y + bottom));
                    else if (y > 0)
                        matrix.postTranslate(0, -y);
                }
            }
            return true;

        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = MeasureSpec.getSize(widthMeasureSpec);
        height = MeasureSpec.getSize(heightMeasureSpec);
        // Fit to screen.
        float scale;
        float scaleX = (float) width / (float) bmWidth;
        float scaleY = (float) height / (float) bmHeight;
        scale = Math.min(scaleX, scaleY);
        matrix.setScale(scale, scale);
        setImageMatrix(matrix);
        saveScale = SAVE_SCALE;
        origScale = scale;

        // Center the image
        redundantYSpace = (float) height - (scale * (float) bmHeight);
        redundantXSpace = (float) width - (scale * (float) bmWidth);
        redundantYSpace /= (float) 2;
        redundantXSpace /= (float) 2;

        origRedundantXSpace = redundantXSpace;
        origRedundantYSpace = redundantYSpace;

        matrix.postTranslate(redundantXSpace, redundantYSpace);

        origWidth = width - 2 * redundantXSpace;
        origHeight = height - 2 * redundantYSpace;
        right = width * saveScale - width - (2 * redundantXSpace * saveScale);
        bottom = height * saveScale - height
                - (2 * redundantYSpace * saveScale);
        origRight = right;
        origBottom = bottom;
        setImageMatrix(matrix);
    }

}
Terence
  • 288
  • 3
  • 3
2

This is a very late addition to this thread but I've been working on an image view that supports zoom and pan and has a couple of features I haven't found elsewhere. This started out as a way of displaying very large images without causing OutOfMemoryErrors, by subsampling the image when zoomed out and loading higher resolution tiles when zoomed in. It now supports use in a ViewPager, rotation manually or using EXIF information (90° stops), override of selected touch events using OnClickListener or your own GestureDetector or OnTouchListener, subclassing to add overlays, pan while zooming, and fling momentum.

It's not intended as a general use replacement for ImageView so doesn't extend it, and doesn't support display of images from resources, only assets and external files. It requires SDK 10.

Source is on GitHub, and there's a sample that illustrates use in a ViewPager.

https://github.com/davemorrissey/subsampling-scale-image-view

Dave Morrissey
  • 4,371
  • 1
  • 23
  • 31
1

You can try using the LayoutParams for this

public void zoom(boolean flag){
    if(flag){
        int width=40;
        int height=40;
    }
    else{
        int width=20;
        int height=20;
    }
    RelativeLayout.LayoutParams param=new RelativeLayout.LayoutParams(width,height); //use the parent layout of the ImageView;
    imageView.setLayoutParams(param); //imageView is the view which needs zooming.
}

ZoomIn = zoom(true); ZoomOut = zoom(false);

0xC0DED00D
  • 19,522
  • 20
  • 117
  • 184
0
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    imageDetail = (ImageView) findViewById(R.id.imageView1);
    imageDetail.setOnTouchListener(new View.OnTouchListener() {

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ImageView view = (ImageView) v;
            System.out.println("matrix=" + savedMatrix.toString());
            switch (event.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    savedMatrix.set(matrix);
                    startPoint.set(event.getX(), event.getY());
                    mode = DRAG;
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    oldDist = spacing(event);
                    if (oldDist > 10f) {
                        savedMatrix.set(matrix);
                        midPoint(midPoint, event);
                        mode = ZOOM;
                    }
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_POINTER_UP:
                    mode = NONE;
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (mode == DRAG) {
                        matrix.set(savedMatrix);
                        matrix.postTranslate(event.getX() - startPoint.x, event.getY() - startPoint.y);
                    } else if (mode == ZOOM) {
                        float newDist = spacing(event);
                        if (newDist > 10f) {
                            matrix.set(savedMatrix);
                            float scale = newDist / oldDist;
                            matrix.postScale(scale, scale, midPoint.x, midPoint.y);
                        }
                    }
                    break;
            }
            view.setImageMatrix(matrix);
            return true;

        }

        @SuppressLint("FloatMath")
        private float spacing(MotionEvent event) {
            float x = event.getX(0) - event.getX(1);
            float y = event.getY(0) - event.getY(1);
            return FloatMath.sqrt(x * x + y * y);
        }

        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);
        }
    });
}

and drawable folder should have bticn image file. perfectly works :)

JohnnyLambada
  • 12,700
  • 11
  • 57
  • 61
Muhammad Usman Ghani
  • 1,279
  • 13
  • 19
0

Something like below will do it.

@Override public boolean onTouch(View v,MotionEvent e)
{

    tap=tap2=drag=pinch=none;
    int mask=e.getActionMasked();
    posx=e.getX();posy=e.getY();

    float midx= img.getWidth()/2f;
    float midy=img.getHeight()/2f;
    int fingers=e.getPointerCount();

    switch(mask)
    {
        case MotionEvent.ACTION_POINTER_UP:
            tap2=1;break;

        case MotionEvent.ACTION_UP:
            tap=1;break;

        case MotionEvent.ACTION_MOVE:
            drag=1;
    }
    if(fingers==2){nowsp=Math.abs(e.getX(0)-e.getX(1));}
    if((fingers==2)&&(drag==0)){ tap2=1;tap=0;drag=0;}
    if((fingers==2)&&(drag==1)){ tap2=0;tap=0;drag=0;pinch=1;}

    if(pinch==1)

    {
        if(nowsp>oldsp)scale+=0.1;
        if(nowsp<oldsp)scale-=0.1;
        tap2=tap=drag=0;    
    }
    if(tap2==1)
        {
            scale-=0.1;
            tap=0;drag=0;
        }
    if(tap==1)
        {
            tap2=0;drag=0;
            scale+=0.1;
        }
    if(drag==1)
        {
            movx=posx-oldx;
            movy=posy-oldy;
            x+=movx;
            y+=movy;
            tap=0;tap2=0;
        }
    m.setTranslate(x,y);
    m.postScale(scale,scale,midx,midy);
    img.setImageMatrix(m);img.invalidate();
    tap=tap2=drag=none;
    oldx=posx;oldy=posy;
    oldsp=nowsp;
    return true;
}


public void onCreate(Bundle b)
{
        super.onCreate(b);

    img=new ImageView(this);
    img.setScaleType(ImageView.ScaleType.MATRIX);
    img.setOnTouchListener(this);

    path=Environment.getExternalStorageDirectory().getPath();   
    path=path+"/DCIM"+"/behala.jpg";
    byte[] bytes;
    bytes=null;
    try{
        FileInputStream fis;
        fis=new FileInputStream(path);
        BufferedInputStream bis;
        bis=new BufferedInputStream(fis);
        bytes=new byte[bis.available()];
        bis.read(bytes);
        if(bis!=null)bis.close();
        if(fis!=null)fis.close();

     }
    catch(Exception e)
        {
        ret="Nothing";
        }
    Bitmap bmp=BitmapFactory.decodeByteArray(bytes,0,bytes.length);

    img.setImageBitmap(bmp);

    setContentView(img);
}

For viewing complete program see here: Program to zoom image in android