Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pinch zooming with ScrollView: Updating its scrolling behaivor when child size changes

I have a custom class which I extended from the ScrollView. What I want to do is to implement pinch zooming with this class, with which I want to zoom to the ScrollView's child and scroll the zoomed content as needed. Basically, I want the same behavior which the UIScrollView class of iOS has (it is a SHAME that Android does not provide any built-in zooming feature like iOS and we have to write everything from scratch). I have a ScaleGestureListener inside of my class and I update the layout dimension of the child view inside of the ScrollView, when I receive onScale events: I multiply the layout width and height with the scale factor I receive from ScaleGestureListener. What bothers me is that the ScrollView does not update its scrollbars according to its child's new dimensions. For example, if the width of the child becomes larger than the ScrollView's, it won't show a horizontal scrollbar. I call invalidate and requestLayout both on the scroll view and its child, but they failed.

In the following I give my code:

public class PinchZoomScrollView extends ScrollView{

private ScaleGestureDetector mScaleDetector;// = new ScaleGestureDetector(context, new ScaleListener());
private float mScaleFactor = 1.0f;
private float referenceTextSize;
private static final float MIN_ZOOM_AMOUNT = 1.0f;
private static final float MAX_ZOOM_AMOUNT = 2.0f;

private float refChildWidth;
private float refChildHeight;

private boolean shownForTheFirstTime = true;
//private View content;


private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

    private WeakReference<PinchZoomScrollView> containerRef;

    public ScaleListener(PinchZoomScrollView container)
    {
        super();

        containerRef = new WeakReference<PinchZoomScrollView>(container);
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        mScaleFactor *= detector.getScaleFactor();

        // Don't let the object get too small or too large.
        mScaleFactor = Math.max(MIN_ZOOM_AMOUNT, Math.min(mScaleFactor, MAX_ZOOM_AMOUNT));

        //Get Child object
        View content = containerRef.get().getChildAt(0);

        if(shownForTheFirstTime)
        {
            shownForTheFirstTime = false;

            refChildWidth  = content.getWidth();
            refChildHeight = content.getHeight();
        }

        LayoutParams params = new LayoutParams(Math.round(mScaleFactor*refChildWidth), Math.round(mScaleFactor*refChildHeight));
        content.setLayoutParams(params);


        System.out.println("this.width="+containerRef.get().getWidth());
        System.out.println("content.width="+Math.round(mScaleFactor*refChildWidth));


        content.invalidate();
        content.requestLayout();

        containerRef.get().updateViewLayout(content, params);

        containerRef.get().invalidate();
        containerRef.get().requestLayout();

        return true;
    }
}


public PinchZoomScrollView(Context context)
{
    super(context);
    mScaleDetector = new ScaleGestureDetector(context, new ScaleListener(this));
}

public PinchZoomScrollView(Context context, AttributeSet attrs)
{
    super(context,attrs);
    mScaleDetector = new ScaleGestureDetector(context, new ScaleListener(this));
}

public PinchZoomScrollView(Context context, AttributeSet attrs, int defStyle)
{
    super(context,attrs,defStyle);
    mScaleDetector = new ScaleGestureDetector(context, new ScaleListener(this));
}

@Override
public boolean onTouchEvent(MotionEvent ev) {

    super.onTouchEvent(ev);

    mScaleDetector.onTouchEvent(ev);

    return true;
}

}

To take long story short, I need the ScrollView to update its scrollbars according to its child's updated size, everytime the user makes a pinching zoom in and out.

Thanks in advance

like image 768
Ufuk Can Bicici Avatar asked Oct 05 '22 11:10

Ufuk Can Bicici


1 Answers

Here is a solution:

  1. Rather than extending ScrollView, I wrote a TwoDimensionScrollView extending FrameLayout which is a combination of ScrollView and HorizontalScrollView so that it can show both horizontal/vertical scrollbars, even if the child is scaled. Also I did not use gesture detector. Instead, I implement my own pinch-to-zoom.

  2. To update scroll bars, rewrite the functions below:

    @Override
    protected int computeVerticalScrollRange() {
        final int count = getChildCount();
        final int contentHeight = getHeight() - getPaddingBottom()
            - getPaddingTop();
        if (count == 0) {
            return contentHeight;
        }
    
    
    @Override
    protected int computeVerticalScrollOffset() {
        int result = Math.max(0, super.computeVerticalScrollOffset());
        return result - getScrollVerticalMin();
    }
    @Override
    protected int computeHorizontalScrollRange() {
        final int count = getChildCount();
        final int contentWidth = getWidth() - getPaddingLeft()
            - getPaddingRight();
        if (count == 0) {
            return contentWidth;
        }
    
        int scrollRange = getChildAt(0).getRight();
        final int scrollX = getScrollX();
        final int overscrollRight = Math.max(0, scrollRange - contentWidth);
        if (scrollX < 0) {
            scrollRange -= scrollX;
        } else if (scrollX > overscrollRight) {
            scrollRange += scrollX - overscrollRight;
        }
        return (int) (scrollRange * mScaleFactor + getScrollHorizontalMin());
    }
    
    @Override
    protected int computeHorizontalScrollOffset() {
        int result = Math.max(0, super.computeHorizontalScrollOffset());
        return result - getScrollHorizontalMin();
    }
    @Override
    protected int computeVerticalScrollExtent() {
        return getHeight() + getScrollVerticalMin();
    }
    
    @Override
    protected int computeHorizontalScrollExtent() {
        return getWidth() + getScrollHorizontalMin();
    }
    private int getScrollHorizontalMin() {
        if (this.getChildCount() < 1)
            return 0;
        View child = this.getChildAt(0);
        return (int) ((1 - mScaleFactor) * child.getPivotX());
    }
    
    private int getScrollVerticalMin() {
        if (this.getChildCount() < 1)
            return 0;
        View child = this.getChildAt(0);
        return (int) ((1 - mScaleFactor) * child.getPivotY());
    }
    private int getScrollVerticalRange() {
        int scrollRange = 0;
        if (getChildCount() > 0) {
            View child = getChildAt(0);
            scrollRange = (int) Math.max(0, child.getHeight() * mScaleFactor
                    - (getHeight() - getPaddingBottom() - getPaddingTop())
                    + getScrollVerticalMin());
        }
    return scrollRange;
    }
    
    private int getScrollHorizontalRange() {
        int scrollRange = 0;
        if (getChildCount() > 0) {
            View child = getChildAt(0);
            scrollRange = (int) Math.max(0, child.getWidth() * mScaleFactor
                    - (getWidth() - getPaddingRight() - getPaddingLeft())
                    + getScrollHorizontalMin());
        }
        return scrollRange;
    }
    
  3. You also need to find all code calling SpringBack() and fling(), and then set the min value for x and y using the above functions getScrollHorizontalMin() and getScrollVerticalMin() instead of 0

like image 169
user2407188 Avatar answered Oct 10 '22 02:10

user2407188