View with Horizontal and Vertical Pan/Drag and Pinch-Zoom

How to create zoomable & pannable view with many draggable views on top of it?

Thanks @PopGorn, who pointed me in the right direction. I've looked around for some examples which included the dispatching of the touch events. I ended up using this nice answer: https://stackoverflow.com/a/12497670/757073

Implementing pinch zoom and drag using Android's build in gesture listener and scale listener

I have implemented this behaviour, and I used a matrix to handle all the zooming and scrolling (and rotation, in my case). It makes for neat code and works like clockwork.

Store a Matrix as a class member:

Matrix drawMatrix;

Edit: Store old focus point, used to get the focus shift during scaling.

float lastFocusX;
float lastFocusY;

Edit: Set lastFocus variables in onScaleBegin

@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
lastFocusX = detector.getFocusX();
lastFocusY = detector.getFocusY();
return true;
}

Replace your onScale:

@Override
public boolean onScale(ScaleGestureDetector detector) {
Matrix transformationMatrix = new Matrix();
float focusX = detector.getFocusX();
float focusY = detector.getFocusY();

//Zoom focus is where the fingers are centered,
transformationMatrix.postTranslate(-focusX, -focusY);

transformationMatrix.postScale(detector.getScaleFactor(), detector.getScaleFactor());

/* Adding focus shift to allow for scrolling with two pointers down. Remove it to skip this functionality. This could be done in fewer lines, but for clarity I do it this way here */
//Edited after comment by chochim
float focusShiftX = focusX - lastFocusX;
float focusShiftY = focusY - lastFocusY;
transformationMatrix.postTranslate(focusX + focusShiftX, focusY + focusShiftY);
drawMatrix.postConcat(transformationMatrix);
lastFocusX = focusX;
lastFocusY = focusY;
invalidate();
return true;
}

Similarly in onScroll:

@Override
public boolean onScroll(MotionEvent downEvent, MotionEvent currentEvent,
float distanceX, float distanceY) {
drawMatrix.postTranslate(-distanceX, -distanceY);
invalidate();
return true;
}

in onDraw; Draw with your drawMatrix:

canvas.drawBitmap(image, drawMatrix, paint);

Happy coding!

Android - zoom in/out RelativeLayout with spread/pinch

So I created a subclass of RelativeLayout as described in the above mentioned topics. It looks like this:

public class ZoomableRelativeLayout extends RelativeLayout {
float mScaleFactor = 1;
float mPivotX;
float mPivotY;

public ZoomableRelativeLayout(Context context) {
super(context);
// TODO Auto-generated constructor stub
}

public ZoomableRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}

public ZoomableRelativeLayout(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}

protected void dispatchDraw(Canvas canvas) {
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.scale(mScaleFactor, mScaleFactor, mPivotX, mPivotY);
super.dispatchDraw(canvas);
canvas.restore();
}

public void scale(float scaleFactor, float pivotX, float pivotY) {
mScaleFactor = scaleFactor;
mPivotX = pivotX;
mPivotY = pivotY;
this.invalidate();
}

public void restore() {
mScaleFactor = 1;
this.invalidate();
}

}

My implementation of the SimpleOnScaleGestureListener looks like this:

private class OnPinchListener extends SimpleOnScaleGestureListener {

float startingSpan;
float endSpan;
float startFocusX;
float startFocusY;

public boolean onScaleBegin(ScaleGestureDetector detector) {
startingSpan = detector.getCurrentSpan();
startFocusX = detector.getFocusX();
startFocusY = detector.getFocusY();
return true;
}

public boolean onScale(ScaleGestureDetector detector) {
mZoomableRelativeLayout.scale(detector.getCurrentSpan()/startingSpan, startFocusX, startFocusY);
return true;
}

public void onScaleEnd(ScaleGestureDetector detector) {
mZoomableRelativeLayout.restore();
}
}

Hope this helps!

Update:

You can integrate OnPinchListener for your ZoomableRelativelayout by using ScaleGestureDetector:

ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(this, new OnPinchListener());

And you are required to bind touch listener of Zoomable layout with the touch listener of ScaleGestureDetector:

mZoomableLayout.setOnTouchListener(new OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
scaleGestureDetector.onTouchEvent(event);
return true;
}
});

View Pager + ImageView +Pinch Zoom + Rotation

EDIT 2: Example code has been pushed to the master branch of TouchImageView. Here is a link to the example activity and a link to the ExtendedViewPager.


EDIT: added code adapting the example link to TouchImageView. Note: you will need the latest code, which is currently in the dev branch. In the future, this will be included in v1.2.0. You know you have the latest code if TouchImageView overrides canScrollHorizontally.

Step 1: Extend ViewPager and override canScroll to call canScrollHorizontallyFroyo.

public class ExtendedViewPager extends ViewPager {

public ExtendedViewPager(Context context) {
super(context);
}

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

@Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof TouchImageView) {
return ((TouchImageView) v).canScrollHorizontallyFroyo(-dx);
} else {
return super.canScroll(v, checkV, dx, x, y);
}
}

}

Step 2: Modify TouchImageView by adding canScrollHorizontallyFroyo:

public boolean canScrollHorizontallyFroyo(int direction) {
return canScrollHorizontally(direction);
}

Step 3: Your activity

public class TouchImageViewActivity extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
ExtendedViewPager mViewPager = (ExtendedViewPager) findViewById(R.id.view_pager);
setContentView(mViewPager);
mViewPager.setAdapter(new TouchImageAdapter());
}

static class TouchImageAdapter extends PagerAdapter {

private static int[] images = { R.drawable.img1, R.drawable.img2, R.drawable.img3 };

@Override
public int getCount() {
return images.length;
}

@Override
public View instantiateItem(ViewGroup container, int position) {
TouchImageView img = new TouchImageView(container.getContext());
img.setImageResource(images[position]);
container.addView(img, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
return img;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}

@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}

}
}

Step 4: main.xml

<com.example.touch.ExtendedViewPager 
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

TouchImageView is actually my project. I currently have a fix in the dev branch for integration with ViewPagers, which will be pushed to master in an upcoming release. Unfortunately, this fix is only applicable for API 14 and greater since honeycomb and earlier do not call canScrollHorizontally. If you need to support older APIs, then you will need to implement a workaround in your ViewPager. Here is an example.

How can I use react-zoom-pan-pinch to allow users to view all of a chart that changes size based on user input?

I discovered the answer to my own question. It turns out the minScale, maxScale, and other props were not being passed to the component properly. Once they are passed properly the package works very well. Here's my explanation/fix

The documentation suggests doing this:


import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";

class Example extends Component {
render() {
return (
<TransformWrapper
initialScale={1}
minScale={0.5}
maxScale={7}
initialPositionX={200}
initialPositionY={100}
>
{({ zoomIn, zoomOut, resetTransform, ...rest }) => (
<React.Fragment>
<div className="tools">
<button onClick={() => zoomIn()}>+</button>
<button onClick={() => zoomOut()}>-</button>
<button onClick={() => resetTransform()}>x</button>
</div>
<TransformComponent>
<img src="image.jpg" alt="test" />
<div>Example text</div>
</TransformComponent>
</React.Fragment>
)}
</TransformWrapper>
);
}
}

The above doesn't work, and the minScale and maxScale props aren't passed to the component. If you open the React dev tools in your browser and go to TransformWrapper, you'll see the default values of 1 for minScale and 8 for maxScale, not the values you entered in your code.

You can solve the problem by creating an object:

  const transformOptions = {
initialScale: 1,
minScale: 0.5,
maxScale: 2
}

Then setting an options prop inside the TransformWrapper component equal to the object, like so:


import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";

class Example extends Component {
render() {
return (
<TransformWrapper
initialScale={1}
options={transformOptions}
initialPositionX={200}
initialPositionY={100}
>
{({ zoomIn, zoomOut, resetTransform, ...rest }) => (
<React.Fragment>
<div className="tools">
<button onClick={() => zoomIn()}>+</button>
<button onClick={() => zoomOut()}>-</button>
<button onClick={() => resetTransform()}>x</button>
</div>
<TransformComponent>
<img src="image.jpg" alt="test" />
<div>Example text</div>
</TransformComponent>
</React.Fragment>
)}
</TransformWrapper>
);
}
}

The same thing applies to pan, wheel, and zoom options. They don't work if set directly in the component as suggested by the documentation, but do work if you create objects like I did above.



Related Topics



Leave a reply



Submit