Jenifer B
Jenifer B

Reputation: 81

Pinch Zoom For LinearLayout in Android

I need to zoom the entire layout contains Image,TextViews etc.i found zoom functionalities only for Imageview.

Upvotes: 7

Views: 13615

Answers (5)

Kuki
Kuki

Reputation: 59

Here's my slightly updated version of Asif's answer:

ZoomLinearLayout.kt:

class ZoomLinearLayout : LinearLayout, OnScaleGestureListener {
    private enum class Mode {
        NONE, DRAG, ZOOM
    }
    private var mode = Mode.NONE
    private var scale = 1.0f
    private var lastScaleFactor = 0f
    private var startX = 0f
    private var startY = 0f
    private var dx = 0f
    private var dy = 0f
    private var prevDx = 0f
    private var prevDy = 0f
    var minZoom: Float = 0.2f
    var maxZoom: Float = 4.0f

    constructor(context: Context?) : super(context) {
        init(context)
    }

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        init(context)
    }

    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(
        context,
        attrs,
        defStyle
    ) {
        init(context)
    }

    fun init(context: Context?) {
        val scaleDetector = ScaleGestureDetector(context!!, this)
        setOnTouchListener { view, motionEvent ->
            when (motionEvent.action and MotionEvent.ACTION_MASK) {
                MotionEvent.ACTION_DOWN -> if (scale > minZoom) {
                    mode = Mode.DRAG
                    startX = motionEvent.x - prevDx
                    startY = motionEvent.y - prevDy
                }

                MotionEvent.ACTION_MOVE -> if (mode == Mode.DRAG) {
                    dx = motionEvent.x - startX
                    dy = motionEvent.y - startY
                }

                MotionEvent.ACTION_POINTER_DOWN -> mode = Mode.ZOOM
                MotionEvent.ACTION_UP -> {
                    mode = Mode.NONE
                    prevDx = dx
                    prevDy = dy
                }
            }
            scaleDetector.onTouchEvent(motionEvent)
            if (mode == Mode.DRAG && scale >= minZoom || mode == Mode.ZOOM) {
                parent.requestDisallowInterceptTouchEvent(true)
                val maxDx = (child().width - child().width / scale) / 2 * scale
                val maxDy = (child().height - child().height / scale) / 2 * scale
                dx = Math.min(Math.max(dx, -maxDx), maxDx)
                dy = Math.min(Math.max(dy, -maxDy), maxDy)
                applyScaleAndTranslation()
            }
            view.performClick()
            true
        }
    }

    override fun onScaleBegin(scaleDetector: ScaleGestureDetector): Boolean {
        return true
    }

    override fun onScale(scaleDetector: ScaleGestureDetector): Boolean {
        val scaleFactor = scaleDetector.scaleFactor
        if (lastScaleFactor == 0f || Math.signum(scaleFactor) == Math.signum(lastScaleFactor)) {
            scale *= scaleFactor
            scale = Math.max(minZoom, Math.min(scale, maxZoom))
            lastScaleFactor = scaleFactor
        } else {
            lastScaleFactor = 0f
        }
        return true
    }

    override fun onScaleEnd(scaleDetector: ScaleGestureDetector) {}
    private fun applyScaleAndTranslation() {
        if (scale < 1.0f) {
            val child = child() // Retrieve the child view
            val childWidth = child.width * scale
            val childHeight = child.height * scale
            val deltaX = (childWidth - child.width) / 2 // Calculate translation for centering
            val deltaY = (childHeight - child.height) / 2
            child.scaleX = scale
            child.scaleY = scale
            child.translationX = dx - deltaX // Apply translation with adjustment
            child.translationY = dy - deltaY

        } else {
            child().scaleX = scale
            child().scaleY = scale
            child().translationX = dx
            child().translationY = dy
        }
    }

    private fun child(): View {
        return getChildAt(0)
    }
}

What are the differences?

  1. I removed the MotionEvent.ACTION_POINTER_UP case, because it made my layout jump to the second finger when I raised one of my fingers from the screen.
  2. The original answer didn't handle good the cases when the scale was less than 1.0. The image (in my case) was sliding to the top left corner, which did't look very good. Now it is centered.
  3. Instead of hardcoding the min/max scale values, I made them public vars, so you don't have to dive into the code.
  4. My solution is in Kotlin :)

How you can use it:

MainActivity.kt:

val zoomView = findViewById(R.id.zoomableLinearLayout)
zoomView.setOnTouchListener { _, _ ->
            zoomView.init(this) //pass a context as the parameter
            false
        }

activity_main.xml:

    <com.example.geoguess.util.ZoomLinearLayout
        android:id="@+id/zoomableLinearLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center">

        </LinearLayout>
    </com.example.geoguess.util.ZoomLinearLayout>

Upvotes: 1

Sanjay Bhalani
Sanjay Bhalani

Reputation: 2484

Create a Custom Layout Class Called ZoomLayout. In this layout i have used Framelayout you can scale all chile layout.

ZoomLayout.java

public class ZoomLayout extends FrameLayout implements ScaleGestureDetector.OnScaleGestureListener {

private enum Mode {
    NONE,
    DRAG,
    ZOOM
}

private static final String TAG = "ZoomLayout";
private static final float MIN_ZOOM = 1.0f;
private static final float MAX_ZOOM = 16.0f;

private Mode mode = Mode.NONE;
private float scale = 1.0f;
private float lastScaleFactor = 0f;

// Where the finger first  touches the screen
private float startX = 0f;
private float startY = 0f;

// How much to translate the canvas
private float dx = 0f;
private float dy = 0f;
private float prevDx = 0f;
private float prevDy = 0f;
ZoomViewListener listener;
public ZoomLayout(Context context) {
    super(context);
    init(context);
    setListner(getListener());
}

public ZoomLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
    setListner(getListener());
}

public ZoomLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init(context);
    setListner(getListener());

}

private void init(Context context) {
    final ScaleGestureDetector scaleDetector = new ScaleGestureDetector(context, this);
    this.setOnTouchListener(new OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:






                    /*float eventX = motionEvent.getX();
                    float eventY = motionEvent.getY();
                    float[] eventXY = new float[]{eventX, eventY};

// float[] src={motionEvent.getX(),motionEvent.getY()}; // float[] dst = new float[2];

                    Matrix invertMatrix = new Matrix();
                    ((ImageView) getChildAt(1)).getImageMatrix().invert(invertMatrix);

                    invertMatrix.mapPoints(eventXY);
                    //invertMatrix.mapPoints(src,dst);
                    int x = Integer.valueOf((int) eventXY[0]);
                    int y = Integer.valueOf((int) eventXY[1]);


                    Log.e("image x", "===img ki x==" + x);
                    Log.e("image y", "===img ki y==" + y);

                    Drawable imgDrawable = ((ImageView) getChildAt(1)).getDrawable();
                    Bitmap bitmap = ((BitmapDrawable) imgDrawable).getBitmap();

                   // int color_value= getHitboxColour(x,y,(ImageView) getChildAt(0));

//Limit x, y range within bitmap

                    if (x < 0) {
                        x = 0;
                    } else if (x > bitmap.getWidth() - 1) {
                        x = bitmap.getWidth() - 1;
                    }

                    if (y < 0) {
                        y = 0;
                    } else if (y > bitmap.getHeight() - 1) {
                        y = bitmap.getHeight() - 1;
                    }


                    //Log.e("touched color: ", "" + "#" + Integer.toHexString(color_value));
                   int touchedRGB = bitmap.getPixel(x, y);

                    int redValue = Color.red(touchedRGB);
                    int blueValue = Color.blue(touchedRGB);
                    int greenValue = Color.green(touchedRGB);

                    Log.e("touched color: ", "" + "#" + Integer.toHexString(touchedRGB));

                    listener.onPlaceChosen(touchedRGB);*/

                    Log.i(TAG, "DOWN");
                    if (scale > MIN_ZOOM) {
                        mode = Mode.DRAG;
                        startX = motionEvent.getX() - prevDx;
                        startY = motionEvent.getY() - prevDy;
                    }





                    break;
                case MotionEvent.ACTION_MOVE:
                    if (mode == Mode.DRAG) {
                        dx = motionEvent.getX() - startX;
                        dy = motionEvent.getY() - startY;

                    }
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    mode = Mode.ZOOM;
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    mode = Mode.DRAG;
                    break;
                case MotionEvent.ACTION_UP:
                    Log.i(TAG, "UP");
                    mode = Mode.NONE;
                    prevDx = dx;
                    prevDy = dy;
                    break;
            }
            scaleDetector.onTouchEvent(motionEvent);

           /* if ((mode == Mode.DRAG && scale >= MIN_ZOOM) || mode == Mode.ZOOM) {
                getParent().requestDisallowInterceptTouchEvent(true);
                float maxDx = (child().getWidth() - (child().getWidth() / scale)) / 2 * scale;
                float maxDy = (child().getHeight() - (child().getHeight() / scale)) / 2 * scale;
                dx = Math.min(Math.max(dx, -maxDx), maxDx);
                dy = Math.min(Math.max(dy, -maxDy), maxDy);
                Log.i(TAG, "Width: " + child().getWidth() + ", scale " + scale + ", dx " + dx
                        + ", max " + maxDx);
                applyScaleAndTranslation();

            }*/

            if (( scale >= MIN_ZOOM) || mode == Mode.ZOOM) {
                getParent().requestDisallowInterceptTouchEvent(true);
                float maxDx = (child().getWidth() - (child().getWidth() / scale)) / 2 * scale;
                float maxDy = (child().getHeight() - (child().getHeight() / scale)) / 2 * scale;
                dx = Math.min(Math.max(dx, -maxDx), maxDx);
                dy = Math.min(Math.max(dy, -maxDy), maxDy);
               // Log.i(TAG, "Width: " + child().getWidth() + ", scale " + scale + ", dx " + dx + ", max " + maxDx);
               // applyScaleAndTranslation();
                child().setScaleX(scale);
                child().setScaleY(scale);

                float maxDx1 = (child2().getWidth() - (child2().getWidth() / scale)) / 2 * scale;
                float maxDy1 = (child2().getHeight() - (child2().getHeight() / scale)) / 2 * scale;
                dx = Math.min(Math.max(dx, -maxDx1), maxDx1);
                dy = Math.min(Math.max(dy, -maxDy1), maxDy1);
               // Log.i(TAG, "Width: " + child2().getWidth() + ", scale " + scale + ", dx " + dx + ", max " + maxDx1);
                // applyScaleAndTranslation();
                child2().setScaleX(scale);
                child2().setScaleY(scale);


            }

            if(mode == Mode.DRAG ){
                getParent().requestDisallowInterceptTouchEvent(true);
                float maxDx = (child().getWidth() - (child().getWidth() / scale)) / 2 * scale;
                float maxDy = (child().getHeight() - (child().getHeight() / scale)) / 2 * scale;
                dx = Math.min(Math.max(dx, -maxDx), maxDx);
                dy = Math.min(Math.max(dy, -maxDy), maxDy);
               // Log.i(TAG, "Width: " + child().getWidth() + ", scale " + scale + ", dx " + dx + ", max " + maxDx);

                child().setTranslationX(dx);
                child().setTranslationY(dy);


                getParent().requestDisallowInterceptTouchEvent(true);
                float maxDx1 = (child2().getWidth() - (child2().getWidth() / scale)) / 2 * scale;
                float maxDy1 = (child2().getHeight() - (child2().getHeight() / scale)) / 2 * scale;
                dx = Math.min(Math.max(dx, -maxDx1), maxDx1);
                dy = Math.min(Math.max(dy, -maxDy1), maxDy1);
               // Log.i(TAG, "Width: " + child2().getWidth() + ", scale " + scale + ", dx " + dx + ", max " + maxDx);

                child2().setTranslationX(dx);
                child2().setTranslationY(dy);
            }



            return true;
        }
    });
}
public int getHitboxColour(int x, int y,ImageView iv) {

   // ImageView iv = (ImageView) findViewById(R.id.img_hitbox);

    Bitmap bmpHotspots;

    int pixel;

// Fix any offsets by the positioning of screen elements such as Activity titlebar.

// This part was causing me issues when I was testing out Bill Lahti's code.

    int[] location = new int[2];

    iv.getLocationOnScreen(location);

    x -= location[0];

    y -= location[1];

// Prevent crashes, return background noise

    if ((x < 0) || (y < 0)) {

        return Color.WHITE;

    }

// Draw the scaled bitmap into memory

    iv.setDrawingCacheEnabled(true);

    bmpHotspots = Bitmap.createBitmap(iv.getDrawingCache());

    iv.setDrawingCacheEnabled(false);



    pixel = bmpHotspots.getPixel(x, y);

    bmpHotspots.recycle();

    return pixel;

}
// ScaleGestureDetector

@Override
public boolean onScaleBegin(ScaleGestureDetector scaleDetector) {
    Log.i(TAG, "onScaleBegin");
    return true;
}

@Override
public boolean onScale(ScaleGestureDetector scaleDetector) {
    float scaleFactor = scaleDetector.getScaleFactor();
    Log.i(TAG, "onScale" + scaleFactor);
    if (lastScaleFactor == 0 || (Math.signum(scaleFactor) == Math.signum(lastScaleFactor))) {
        scale *= scaleFactor;
        scale = Math.max(MIN_ZOOM, Math.min(scale, MAX_ZOOM));
        lastScaleFactor = scaleFactor;
    } else {
        lastScaleFactor = 0;
    }
    return true;
}

@Override
public void onScaleEnd(ScaleGestureDetector scaleDetector) {
    Log.i(TAG, "onScaleEnd");
}

private void applyScaleAndTranslation() {
    child().setScaleX(scale);
    child().setScaleY(scale);
    child().setTranslationX(dx);
    child().setTranslationY(dy);




}

private View child() {
    return getChildAt(0);
}


private View child2() {
    return getChildAt(1);
}
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);

    void onPlaceChosen(int color);
}
public ZoomViewListener getListener() {
    return listener;
}

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

Upvotes: 1

Asif Patel
Asif Patel

Reputation: 1764

Create a Custom Layout Class Called ZoomeLinearLayout.

ZoomLinearLayout.java

public class ZoomLinearLayout extends LinearLayout implements ScaleGestureDetector.OnScaleGestureListener {

    private enum Mode {
        NONE,
        DRAG,
        ZOOM
    }

    private static final float MIN_ZOOM = 1.0f;
    private static final float MAX_ZOOM = 4.0f;

    private Mode mode = Mode.NONE;
    private float scale = 1.0f;
    private float lastScaleFactor = 0f;

    private float startX = 0f;
    private float startY = 0f;

    private float dx = 0f;
    private float dy = 0f;
    private float prevDx = 0f;
    private float prevDy = 0f;

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

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

    public ZoomLinearLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    public void init(Context context) {
        final ScaleGestureDetector scaleDetector = new ScaleGestureDetector(context, this);
        this.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
                    case MotionEvent.ACTION_DOWN:
                        if (scale > MIN_ZOOM) {
                            mode = Mode.DRAG;
                            startX = motionEvent.getX() - prevDx;
                            startY = motionEvent.getY() - prevDy;
                        }
                        break;
                    case MotionEvent.ACTION_MOVE:
                        if (mode == Mode.DRAG) {
                            dx = motionEvent.getX() - startX;
                            dy = motionEvent.getY() - startY;
                        }
                        break;
                    case MotionEvent.ACTION_POINTER_DOWN:
                        mode = Mode.ZOOM;
                        break;
                    case MotionEvent.ACTION_POINTER_UP:
                        mode = Mode.DRAG;
                        break;
                    case MotionEvent.ACTION_UP:
                        mode = Mode.NONE;
                        prevDx = dx;
                        prevDy = dy;
                        break;
                }
                scaleDetector.onTouchEvent(motionEvent);

                if ((mode == Mode.DRAG && scale >= MIN_ZOOM) || mode == Mode.ZOOM) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                    float maxDx = (child().getWidth() - (child().getWidth() / scale)) / 2 * scale;
                    float maxDy = (child().getHeight() - (child().getHeight() / scale)) / 2 * scale;
                    dx = Math.min(Math.max(dx, -maxDx), maxDx);
                    dy = Math.min(Math.max(dy, -maxDy), maxDy);
                    applyScaleAndTranslation();
                }

                return true;
            }
        });
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector scaleDetector) {
        return true;
    }

    @Override
    public boolean onScale(ScaleGestureDetector scaleDetector) {
        float scaleFactor = scaleDetector.getScaleFactor();
        if (lastScaleFactor == 0 || (Math.signum(scaleFactor) == Math.signum(lastScaleFactor))) {
            scale *= scaleFactor;
            scale = Math.max(MIN_ZOOM, Math.min(scale, MAX_ZOOM));
            lastScaleFactor = scaleFactor;
        } else {
            lastScaleFactor = 0;
        }
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector scaleDetector) {
    }

    private void applyScaleAndTranslation() {
        child().setScaleX(scale);
        child().setScaleY(scale);
        child().setTranslationX(dx);
        child().setTranslationY(dy);
    }

    private View child() {
        return getChildAt(0);
    }

}

Then in Layout file, Wrap your LinearLayout which you want to zoom with ZoomLinearLayout. Note that you have only one direct child for ZoomLinearLayout.

 <com.asif.test.ZoomLinearLayout
    android:layout_width="match_parent"
    android:id="@+id/zoom_linear_layout"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </LinearLayout>

</com.asif.test.ZoomLinearLayout>

Now in Activity, create ZoomLinearLayout object and set the onTouch() event for it.

final ZoomLinearLayout zoomLinearLayout = (ZoomLinearLayout) findViewById(R.id.zoom_linear_layout);
zoomLinearLayout.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        zoomLinearLayout.init(MainActivity.this);
        return false;
    }
});

Upvotes: 12

Arjun saini
Arjun saini

Reputation: 4182

Xml where layout

   <LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_marginBottom="20dp"
    android:layout_centerHorizontal="true"
    android:id="@+id/llzoom"
    android:orientation="vertical">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@mipmap/ic_launcher"/>
    <TextView
        android:id="@+id/text"
        android:layout_width="100dp"
        android:layout_height="100dp"

        android:layout_marginTop="20dp"

        android:text="School"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        android:textColor="@android:color/black"
        android:textStyle="bold" />
     </LinearLayout>

import

import android.view.animation.Animation;
import android.view.animation.AnimationUtils;

For Zoomout animation

   findViewById(R.id.llzoom).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Animation zoomout = AnimationUtils.loadAnimation(SelectBuyerActivity.this, R.anim.zoomout);
            findViewById(R.id.llzoom).startAnimation(zoomout);
        }
    });

Animation in your res/anim folder

zoomout.xml

<scale  xmlns:android="http://schemas.android.com/apk/res/android"
 android:interpolator="@android:anim/bounce_interpolator" 
 android:fromXScale="0.5" 
 android:toXScale="1" 
 android:fromYScale="0.5" 
 android:toYScale="1" 
 android:pivotX="50%" 
 android:pivotY="50%" 
 android:duration="500" 
 android:fillAfter="true">
 </scale>

You used also more animation Like down

slide_down.xml

 <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <translate
        android:duration="200"
        android:fromYDelta="0%p"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toYDelta="100%p" />
</set>

slide_up.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <translate
        android:duration="200"
        android:fromYDelta="100%p"
        android:toYDelta="0" />
</set>

Upvotes: 0

Erik Minarini
Erik Minarini

Reputation: 4845

The layout doesn't have the zoom by default. I fund this, https://code.google.com/archive/p/android-zoom-view/downloads

In this answer, the user explains well how to use it. https://stackoverflow.com/a/15850113/6093353

Hope this helps you!

Upvotes: 1

Related Questions