Pinch-zoom on a ListView
Asked Answered
B

2

3

I have a ListView which contains a few TextView's arranged using a custom adapter.

What I would like to do, is implement pinch-to-zoom on this ListView, so that when the user pinches, they can increase or decrease the size of the text.

Does anyone know of examples of doing something similar to this? I tried following the code here: Pinch zoom for custom view, but I ended up with a ListView that would not respond to any touch events except the pinch gesture.

I'm hoping this is possible without writing a new class that extends ListView.

Bandler answered 19/11, 2011 at 19:5 Comment(0)
U
2

You should be able to follow the Pinch zoom for custom view and then do something like this when overriding the onTouchEvent. This will allow you to listen for a scale event, as well as all other touch events in the listview.

@Override
public boolean onTouchEvent(MotionEvent ev) {
    // Let the ScaleGestureDetector inspect all events.
    mScaleDetector.onTouchEvent(ev);
    return super.onTouchEvent(ev);
}    
Uda answered 5/12, 2012 at 18:53 Comment(1)
The pinch zoom for custom views deals only with transforming the drawing scale. ListViews have the component-recycling mechanism so when you zoom out, as more rows should become visible, they should be created on the spot, while zooming. I don't think this is handled in the referenced code.Malaya
J
1

Yes we can do this.

/**
 * Zooming view.
 */
public class ZoomView extends FrameLayout {

/**
 * Zooming view listener interface.
 * 
 * @author karooolek
 * 
 */
public interface ZoomViewListener {

    void onZoomStarted(float zoom, float zoomx, float zoomy);

    void onZooming(float zoom, float zoomx, float zoomy);

    void onZoomEnded(float zoom, float zoomx, float zoomy);
}

// zooming
float zoom = 1.0f;
float maxZoom = 2.0f;
float smoothZoom = 1.0f;
float zoomX, zoomY;
float smoothZoomX, smoothZoomY;
private boolean scrolling; // NOPMD by karooolek on 29.06.11 11:45

// minimap variables
private boolean showMinimap = false;
private int miniMapColor = Color.BLACK;
private int miniMapHeight = -1;
private String miniMapCaption;
private float miniMapCaptionSize = 10.0f;
private int miniMapCaptionColor = Color.WHITE;

// touching variables
private long lastTapTime;
private float touchStartX, touchStartY;
private float touchLastX, touchLastY;
private float startd;
private boolean pinching;
private float lastd;
private float lastdx1, lastdy1;
private float lastdx2, lastdy2;

// drawing
private final Matrix m = new Matrix();
private final Paint p = new Paint();

// listener
ZoomViewListener listener;

private Bitmap ch;

public ZoomView(final Context context) {
    super(context);
}

public float getZoom() {
    return zoom;
}

public float getMaxZoom() {
    return maxZoom;
}

public void setMaxZoom(final float maxZoom) {
    if (maxZoom < 1.0f) {
        return;
    }

    this.maxZoom = maxZoom;
}

public void setMiniMapEnabled(final boolean showMiniMap) {
    this.showMinimap = showMiniMap;
}

public boolean isMiniMapEnabled() {
    return showMinimap;
}

public void setMiniMapHeight(final int miniMapHeight) {
    if (miniMapHeight < 0) {
        return;
    }
    this.miniMapHeight = miniMapHeight;
}

public int getMiniMapHeight() {
    return miniMapHeight;
}

public void setMiniMapColor(final int color) {
    miniMapColor = color;
}

public int getMiniMapColor() {
    return miniMapColor;
}

public String getMiniMapCaption() {
    return miniMapCaption;
}

public void setMiniMapCaption(final String miniMapCaption) {
    this.miniMapCaption = miniMapCaption;
}

public float getMiniMapCaptionSize() {
    return miniMapCaptionSize;
}

public void setMiniMapCaptionSize(final float size) {
    miniMapCaptionSize = size;
}

public int getMiniMapCaptionColor() {
    return miniMapCaptionColor;
}

public void setMiniMapCaptionColor(final int color) {
    miniMapCaptionColor = color;
}

public void zoomTo(final float zoom, final float x, final float y) {
    this.zoom = Math.min(zoom, maxZoom);
    zoomX = x;
    zoomY = y;
    smoothZoomTo(this.zoom, x, y);
}

public void smoothZoomTo(final float zoom, final float x, final float y) {
    smoothZoom = clamp(1.0f, zoom, maxZoom);
    smoothZoomX = x;
    smoothZoomY = y;
    if (listener != null) {
        listener.onZoomStarted(smoothZoom, x, y);
    }
}

public ZoomViewListener getListener() {
    return listener;
}

public void setListner(final ZoomViewListener listener) {
    this.listener = listener;
}

public float getZoomFocusX() {
    return zoomX * zoom;
}

public float getZoomFocusY() {
    return zoomY * zoom;
}

@Override
public boolean dispatchTouchEvent(final MotionEvent ev) {
    // single touch
    if (ev.getPointerCount() == 1) {
        processSingleTouchEvent(ev);
    }

    // // double touch
    if (ev.getPointerCount() == 2) {
        processDoubleTouchEvent(ev);
    }

    // redraw
    getRootView().invalidate();
    invalidate();

    return true;
}

private void processSingleTouchEvent(final MotionEvent ev) {

    final float x = ev.getX();
    final float y = ev.getY();

    final float w = miniMapHeight * (float) getWidth() / getHeight();
    final float h = miniMapHeight;
    final boolean touchingMiniMap = x >= 10.0f && x <= 10.0f + w
            && y >= 10.0f && y <= 10.0f + h;

    if (showMinimap && smoothZoom > 1.0f && touchingMiniMap) {
        processSingleTouchOnMinimap(ev);
    } else {
        processSingleTouchOutsideMinimap(ev);
    }
}

private void processSingleTouchOnMinimap(final MotionEvent ev) {
    final float x = ev.getX();
    final float y = ev.getY();

    final float w = miniMapHeight * (float) getWidth() / getHeight();
    final float h = miniMapHeight;
    final float zx = (x - 10.0f) / w * getWidth();
    final float zy = (y - 10.0f) / h * getHeight();
    smoothZoomTo(smoothZoom, zx, zy);
}

private void processSingleTouchOutsideMinimap(final MotionEvent ev) {
    final float x = ev.getX();
    final float y = ev.getY();
    float lx = x - touchStartX;
    float ly = y - touchStartY;
    final float l = (float) Math.hypot(lx, ly);
    float dx = x - touchLastX;
    float dy = y - touchLastY;
    touchLastX = x;
    touchLastY = y;

    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
        touchStartX = x;
        touchStartY = y;
        touchLastX = x;
        touchLastY = y;
        dx = 0;
        dy = 0;
        lx = 0;
        ly = 0;
        scrolling = false;
        break;

    case MotionEvent.ACTION_MOVE:
        if (scrolling || (smoothZoom > 1.0f && l > 30.0f)) {
            if (!scrolling) {
                scrolling = true;
                ev.setAction(MotionEvent.ACTION_CANCEL);
                super.dispatchTouchEvent(ev);
            }
            smoothZoomX -= dx / zoom;
            smoothZoomY -= dy / zoom;
            return;
        }
        break;

    case MotionEvent.ACTION_OUTSIDE:
    case MotionEvent.ACTION_UP:

        // tap
        if (l < 30.0f) {
            // check double tap
            if (System.currentTimeMillis() - lastTapTime < 500) {
                if (smoothZoom == 1.0f) {
                    smoothZoomTo(maxZoom, x, y);
                } else {
                    smoothZoomTo(1.0f, getWidth() / 2.0f,
                            getHeight() / 2.0f);
                }
                lastTapTime = 0;
                ev.setAction(MotionEvent.ACTION_CANCEL);
                super.dispatchTouchEvent(ev);
                return;
            }

            lastTapTime = System.currentTimeMillis();

            performClick();
        }
        break;

    default:
        break;
    }

    ev.setLocation(zoomX + (x - 0.5f * getWidth()) / zoom, zoomY
            + (y - 0.5f * getHeight()) / zoom);

    ev.getX();
    ev.getY();

    super.dispatchTouchEvent(ev);
}

private void processDoubleTouchEvent(final MotionEvent ev) {
    final float x1 = ev.getX(0);
    final float dx1 = x1 - lastdx1;
    lastdx1 = x1;
    final float y1 = ev.getY(0);
    final float dy1 = y1 - lastdy1;
    lastdy1 = y1;
    final float x2 = ev.getX(1);
    final float dx2 = x2 - lastdx2;
    lastdx2 = x2;
    final float y2 = ev.getY(1);
    final float dy2 = y2 - lastdy2;
    lastdy2 = y2;

    // pointers distance
    final float d = (float) Math.hypot(x2 - x1, y2 - y1);
    final float dd = d - lastd;
    lastd = d;
    final float ld = Math.abs(d - startd);

    Math.atan2(y2 - y1, x2 - x1);
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
        startd = d;
        pinching = false;
        break;

    case MotionEvent.ACTION_MOVE:
        if (pinching || ld > 30.0f) {
            pinching = true;
            final float dxk = 0.5f * (dx1 + dx2);
            final float dyk = 0.5f * (dy1 + dy2);
            smoothZoomTo(Math.max(1.0f, zoom * d / (d - dd)), zoomX - dxk
                    / zoom, zoomY - dyk / zoom);
        }

        break;

    case MotionEvent.ACTION_UP:
    default:
        pinching = false;
        break;
    }

    ev.setAction(MotionEvent.ACTION_CANCEL);
    super.dispatchTouchEvent(ev);
}

private float clamp(final float min, final float value, final float max) {
    return Math.max(min, Math.min(value, max));
}

private float lerp(final float a, final float b, final float k) {
    return a + (b - a) * k;
}

private float bias(final float a, final float b, final float k) {
    return Math.abs(b - a) >= k ? a + k * Math.signum(b - a) : b;
}

@Override
protected void dispatchDraw(final Canvas canvas) {
    // do zoom
    zoom = lerp(bias(zoom, smoothZoom, 0.05f), smoothZoom, 0.2f);
    smoothZoomX = clamp(0.5f * getWidth() / smoothZoom, smoothZoomX,
            getWidth() - 0.5f * getWidth() / smoothZoom);
    smoothZoomY = clamp(0.5f * getHeight() / smoothZoom, smoothZoomY,
            getHeight() - 0.5f * getHeight() / smoothZoom);

    zoomX = lerp(bias(zoomX, smoothZoomX, 0.1f), smoothZoomX, 0.35f);
    zoomY = lerp(bias(zoomY, smoothZoomY, 0.1f), smoothZoomY, 0.35f);
    if (zoom != smoothZoom && listener != null) {
        listener.onZooming(zoom, zoomX, zoomY);
    }

    final boolean animating = Math.abs(zoom - smoothZoom) > 0.0000001f
            || Math.abs(zoomX - smoothZoomX) > 0.0000001f
            || Math.abs(zoomY - smoothZoomY) > 0.0000001f;

    // nothing to draw
    if (getChildCount() == 0) {
        return;
    }

    // prepare matrix
    m.setTranslate(0.5f * getWidth(), 0.5f * getHeight());
    m.preScale(zoom, zoom);
    m.preTranslate(
            -clamp(0.5f * getWidth() / zoom, zoomX, getWidth() - 0.5f
                    * getWidth() / zoom),
            -clamp(0.5f * getHeight() / zoom, zoomY, getHeight() - 0.5f
                    * getHeight() / zoom));

    // get view
    final View v = getChildAt(0);
    m.preTranslate(v.getLeft(), v.getTop());

    // get drawing cache if available
    if (animating && ch == null && isAnimationCacheEnabled()) {
        v.setDrawingCacheEnabled(true);
        ch = v.getDrawingCache();
    }

    // draw using cache while animating
    if (animating && isAnimationCacheEnabled() && ch != null) {
        p.setColor(0xffffffff);
        canvas.drawBitmap(ch, m, p);
    } else { // zoomed or cache unavailable
        ch = null;
        canvas.save();
        canvas.concat(m);
        v.draw(canvas);
        canvas.restore();
    }

    // draw minimap
    if (showMinimap) {
        if (miniMapHeight < 0) {
            miniMapHeight = getHeight() / 4;
        }

        canvas.translate(10.0f, 10.0f);

        p.setColor(0x80000000 | 0x00ffffff & miniMapColor);
        final float w = miniMapHeight * (float) getWidth() / getHeight();
        final float h = miniMapHeight;
        canvas.drawRect(0.0f, 0.0f, w, h, p);

        if (miniMapCaption != null && miniMapCaption.length() > 0) {
            p.setTextSize(miniMapCaptionSize);
            p.setColor(miniMapCaptionColor);
            p.setAntiAlias(true);
            canvas.drawText(miniMapCaption, 10.0f,
                    10.0f + miniMapCaptionSize, p);
            p.setAntiAlias(false);
        }

        p.setColor(0x80000000 | 0x00ffffff & miniMapColor);
        final float dx = w * zoomX / getWidth();
        final float dy = h * zoomY / getHeight();
        canvas.drawRect(dx - 0.5f * w / zoom, dy - 0.5f * h / zoom, dx
                + 0.5f * w / zoom, dy + 0.5f * h / zoom, p);

        canvas.translate(-10.0f, -10.0f);
    }

    // redraw
    // if (animating) {
    getRootView().invalidate();
    invalidate();
    // }
}
}

when you want to use it make your listview dynamically and add it to zoomview like this.

bodyContainer = (LinearLayout) findViewById(R.id.container);

    lvDateList = new ListView(ListDateActivity.this);
    lvDateList.setLayoutParams(new LinearLayout.LayoutParams(
            LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));

    ZoomView zoomView = new ZoomView(ListDateActivity.this);
    zoomView.setLayoutParams(new LinearLayout.LayoutParams(
            LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    zoomView.addView(lvDateList);

    container.addView(zoomView);
Joshia answered 15/9, 2014 at 10:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.