I need to zoom the entire layout contains Image,TextViews etc.i found zoom functionalities only for Imageview.
5 Answers
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;
}
});

- 1,744
- 1
- 20
- 27
-
my app has multiple fragments, should I use `zoomLinearLayout.init(getActivity().this)`? – dan May 03 '21 at 20:24
-
forgot to mention that the fragments have a ScrollView in case that interferes – dan May 03 '21 at 20:32
-
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!

- 1
- 1

- 4,795
- 15
- 19
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;
}}

- 2,424
- 18
- 44
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>

- 4,223
- 3
- 23
- 51
-
Thanks for your answer.Am asking for pinch Zoom not for click event. – Jenifer B Jul 14 '16 at 13:47
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?
- 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.
- 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.
- Instead of hardcoding the min/max scale values, I made them public vars, so you don't have to dive into the code.
- 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>