1

I'm trying to implement a touch responsive image view - like the one in the Gallery application. I've managed to do that, and it works well, except that it is VERY laggy compared to the stock Gallery.

I'm looking for ways to make it smoother. Here's what I have now:

@SuppressWarnings("unused")
public class ImageDisplayView extends ImageView {
    private static final String TAG = "ImageDisplayView";

private static final int MODE_NONE = 0;
private static final int MODE_DRAG = 1;
private static final int MODE_ZOOM = 2;

// Zoom Bounds
private static final float SCALE_MIN = 0.8f;
private static final float SCALE_BOTTOM = 1.0f;
private static final float SCALE_TOP = 10.0f;
private static final float SCALE_MAX = 12.0f;

// Transformation
private Matrix mMatrix = new Matrix();
private Matrix mPreMatrix = new Matrix();
private PointF mPivot = new PointF();
private PointF mNewPivot = new PointF();
private float mDist;

// State
private boolean mInitialScaleDone = false;
private boolean mEnabled = true;
private RectF mBounds = new RectF();
private Float mScale = null;
private float mMode = MODE_NONE;

public ImageDisplayView(Context context) {
    super(context);
    initialScale();
}

public ImageDisplayView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initialScale();
}

public ImageDisplayView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initialScale();
}

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

private void initialScale() {
    if (!mInitialScaleDone && getDrawable() != null) {
        mInitialScaleDone = true;
        final Runnable r = new Runnable() {

            @Override
            public void run() {
                // Fit to screen
                float scale = calculateFitScreenScale();
                mMatrix.reset();
                mMatrix.postScale(scale, scale);
                setImageMatrix(mMatrix);
                // Center
                float dx = (getWidth() - mBounds.width()) / 2, dy = (getHeight() - mBounds
                        .height()) / 2;
                mMatrix.postTranslate(dx, dy);
                setImageMatrix(mMatrix);
                mScale = 1.0f;
            }
        };
        if (getWidth() == 0) {
            getViewTreeObserver().addOnGlobalLayoutListener(
                    new OnGlobalLayoutListener() {
                        @Override
                        public void onGlobalLayout() {
                            r.run();
                            getViewTreeObserver()
                                    .removeGlobalOnLayoutListener(this);
                        }
                    });
        } else {
            r.run();
        }
    }
}

@Override
public void setImageMatrix(Matrix matrix) {
    super.setImageMatrix(matrix);
    mBounds.set(getDrawable().getBounds());
    matrix.mapRect(mBounds);
}

@Override
public void setEnabled(boolean enabled) {
    mEnabled = enabled;
    if (!enabled && mMode != MODE_NONE) {
        if (mMode == MODE_DRAG) {
            checkLimits(null);
        } else {
            mPivot.set(getWidth() / 2, getHeight() / 2);
            updateScale();
            mPreMatrix.set(mMatrix);
            checkLimits(null);
        }
        mMode = MODE_NONE;
    }
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    float scale, dx, dy;
    if (mEnabled) {
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_POINTER_DOWN:
            if (event.getPointerCount() == 1) { // Start mode: drag only.
                mMode = MODE_DRAG;
                mPivot.set(event.getX(), event.getY());
            } else { // Start mode: zoom and drag.
                mMode = MODE_ZOOM;
                mDist = distance(event);
                midPoint(mPivot, event);
            }
            mPreMatrix.set(mMatrix);
            return true;
        case MotionEvent.ACTION_UP:
            mMode = MODE_NONE;
            checkLimits(event);
            break;
        case MotionEvent.ACTION_POINTER_UP:
            if (event.getPointerCount() == 2) {
                mMode = MODE_DRAG;
                mPivot.set(event.getX(), event.getY());
                updateScale();
                mPreMatrix.set(mMatrix);
            }
            return true;
        case MotionEvent.ACTION_MOVE:
            mMatrix.set(mPreMatrix);
            if (mMode == MODE_DRAG) {
                dx = event.getX() - mPivot.x;
                dy = event.getY() - mPivot.y;
                mMatrix.postTranslate(dx, dy);
            } else if (mMode == MODE_ZOOM) {
                scale = distance(event) / mDist;
                midPoint(mNewPivot, event);
                dx = mNewPivot.x - mPivot.x;
                dy = mNewPivot.y - mPivot.y;
                mMatrix.postTranslate(dx, dy);
                float postScale = mScale * scale;
                if (postScale < SCALE_MIN) {
                    scale = SCALE_MIN / mScale;
                } else if (postScale > SCALE_MAX) {
                    scale = SCALE_MAX / mScale;
                }
                mMatrix.postScale(scale, scale, mNewPivot.x, mNewPivot.y);
            }
            setImageMatrix(mMatrix);
            return true;
        }
    }
    return super.onTouchEvent(event);
}

private void updateScale() {
    mScale = (mBounds.width() / getDrawable().getIntrinsicWidth())
            / calculateFitScreenScale();
}

@IntendedCaller("onTouchEvent(MotionEvent)")
private void checkLimits(MotionEvent event) {
    float scale, dx, dy;
    if (mScale < SCALE_BOTTOM) {
        scale = SCALE_BOTTOM;
    } else if (mScale > SCALE_TOP) {
        scale = SCALE_TOP;
    } else {
        scale = mScale;
    }
    RectF scaleBounds = new RectF(mBounds); // Use the scale of the image
                                            // to determine drag
                                            // threshold.
    scaleBounds.offset(getWidth() / 2 - scaleBounds.centerX(), getHeight()
            / 2 - scaleBounds.centerY());
    dx = Math.min(0.0f, Math.max(0.0f, scaleBounds.left) - mBounds.left)
            + Math.max(0.0f, Math.min(getWidth(), scaleBounds.right)
                    - mBounds.right);
    dy = Math.min(0.0f, Math.max(0.0f, scaleBounds.top) - mBounds.top)
            + Math.max(0.0f, Math.min(getHeight(), scaleBounds.bottom)
                    - mBounds.bottom);
    animateTransformation(event, 500, null, scale, dx, dy);
}

public void animateTransformation(MotionEvent event, long duration,
        final Runnable callback, final float targetScale, final float dx,
        final float dy) {
    mPreMatrix.set(mMatrix);
    final float scale = targetScale / mScale;
    final float px, py;
    if (event != null) {
        PointF pivot = new PointF();
        midPoint(pivot, event);
        px = scale > 1.0f ? mBounds.centerX() : dx > 0 ? mBounds.right
                : dx < 0 ? mBounds.left : pivot.x;
        py = scale > 1.0f ? mBounds.centerY() : dy > 0 ? mBounds.bottom
                : dy < 0 ? mBounds.top : pivot.y;
    } else {
        px = mBounds.centerX();
        py = mBounds.centerY();
    }
    Utils.animate(this, new Animator() {

        @Override
        public void onAnimationEnd() {
            updateScale();
            if (callback != null) {
                callback.run();
            }
        }

        @Override
        public void makeStep(float percent) {
            mMatrix.set(mPreMatrix);
            float s = 1.0f + percent * (scale - 1.0f);
            float tempx = dx * percent, tempy = dy * percent;
            mMatrix.postTranslate(tempx, tempy);
            mMatrix.postScale(s, s, px + tempx, py + tempy);
            setImageMatrix(mMatrix);
        }
    }, duration);
}

public RectF getBounds() {
    return mBounds;
}

public float getScale() {
    return mScale;
}

public void scale(float scale, float px, float py) {
    mMatrix.set(getImageMatrix());
    mMatrix.postScale(scale, scale, px, py);
    setImageMatrix(mMatrix);
    mScale = scale;
}

public void translate(float dx, float dy) {
    mMatrix.set(getImageMatrix());
    mMatrix.postTranslate(dx, dy);
    setImageMatrix(mMatrix);
}

public void resetAndCenter() {
    mMatrix.set(getImageMatrix());
    mMatrix.postScale(1 / mScale, 1 / mScale, mBounds.centerX(),
            mBounds.centerY());
    setImageMatrix(mMatrix);
    // Center
    float dx = (getWidth() - mBounds.width()) / 2, dy = (getHeight() - mBounds
            .height()) / 2;
    mMatrix.postTranslate(dx, dy);
    setImageMatrix(mMatrix);
    updateScale();
}

@Override
public void setImageBitmap(Bitmap bm) {
    super.setImageBitmap(bm);
    mInitialScaleDone = false;
    initialScale();
}

private float distance(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 p, MotionEvent event) {
    if (event.getPointerCount() == 1) {
        p.set(event.getX(), event.getY());
    } else {
        p.set((event.getX(1) + event.getX(0)) / 2,
                (event.getY(1) + event.getY(0)) / 2);
    }
}

private float calculateFitScreenScale() {
    RectF r = new RectF(getDrawable().getBounds());
    float w = getWidth(), h = getHeight();
    if (r.width() > r.height()) {
        return w / r.width();
    } else if (r.width() == r.height()) {
        if (w < h) {
            return w / r.width();
        } else {
            return h / r.height();
        }
    } else {
        return h / r.height();
    }
}
}

What can I do in order to make this less laggy? Any help much appreciated.

saarraz1
  • 2,999
  • 6
  • 29
  • 44

3 Answers3

0

Have you tried it on an actual device? The emulator in notoriously slow.

Bill Gary
  • 2,987
  • 2
  • 15
  • 19
  • Tested on Samsung Galaxy Vibrant, on which the Gallery version runs perfectly smooth, while my version is laggy. – saarraz1 Dec 22 '11 at 17:36
0

Here's something that might help:

Check out the Batching section at the Android Dev Resource page:

http://developer.android.com/reference/android/view/MotionEvent.html

I've noticed that how a device batches together historical points in a continuous motion can differ considerably. This may also differ considerably between devices. In your code, you only use getX and getY, which takes the most recent event, ignoring all the historical points. If your device happens to be batching large amounts of historical events together, using only the most recent ones (via getX and getY) might cause lag.

The link suggests how one might consume all the historical points.

You could implement a solution that checks whether too many historical points have been batched together (event.getHistorySize() > x). If the number of historical points are above a threshold of your choosing, make intermediate frame updates based on, say, historySize / 2. Something like that.

uhmdown
  • 1,311
  • 1
  • 9
  • 8
0

Please review this question here How can I get zoom functionality for images?

theres answer by Mike Ortiz down, implementing Mutli-Touch ImageView

Community
  • 1
  • 1
Mohammad Ersan
  • 12,304
  • 8
  • 54
  • 77