2

I am working on a pinch-to-zoom RecyclerView and have the pinch-to-zoom functionality working, but once zoomed in, the onItemClickListeners are in the wrong locations. Below is the code for the modified RecyclerView. I also made a simple demo that is hosted on GitHub. When trying the demo, when you click on an item, you should get a Toast that says the number of the icon you click on. Try it without zooming, then zoom in and try again to see the issue. I believe that I need to notify the RecyclerView that the onItemClickListener locations need to be updated, but cannot find the correct method to override. Thanks for any help!

public class PinchRecyclerView extends RecyclerView {
    private static final int INVALID_POINTER_ID = -1;
    private int mActivePointerId = INVALID_POINTER_ID;
    private ScaleGestureDetector mScaleDetector;
    private float mScaleFactor = 1.f;
    private float maxWidth = 0.0f;
    private float maxHeight = 0.0f;
    private float mLastTouchX;
    private float mLastTouchY;
    private float mPosX;
    private float mPosY;
    private float width;
    private float height;


    public PinchRecyclerView(Context context) {
        super(context);
        if (!isInEditMode())
            mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
    }

    public PinchRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        if (!isInEditMode())
            mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
    }

    public PinchRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        if (!isInEditMode())
            mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        width = View.MeasureSpec.getSize(widthMeasureSpec);
        height = View.MeasureSpec.getSize(heightMeasureSpec);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        try {
            return super.onInterceptTouchEvent(ev);
        } catch (IllegalArgumentException ex) {
            ex.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent ev) {
        super.onTouchEvent(ev);
        final int action = ev.getAction();
        mScaleDetector.onTouchEvent(ev);
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();
                mLastTouchX = x;
                mLastTouchY = y;
                mActivePointerId = ev.getPointerId(0);
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                final float x = ev.getX(pointerIndex);
                final float y = ev.getY(pointerIndex);
                final float dx = x - mLastTouchX;
                final float dy = y - mLastTouchY;

                mPosX += dx;
                mPosY += dy;

                if (mPosX > 0.0f)
                    mPosX = 0.0f;
                else if (mPosX < maxWidth)
                    mPosX = maxWidth;

                if (mPosY > 0.0f)
                    mPosY = 0.0f;
                else if (mPosY < maxHeight)
                    mPosY = maxHeight;

                mLastTouchX = x;
                mLastTouchY = y;

                invalidate();
                break;
            }

            case MotionEvent.ACTION_UP: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }

            case MotionEvent.ACTION_CANCEL: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }

            case MotionEvent.ACTION_POINTER_UP: {
                final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                final int pointerId = ev.getPointerId(pointerIndex);
                if (pointerId == mActivePointerId) {
                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                    mLastTouchX = ev.getX(newPointerIndex);
                    mLastTouchY = ev.getY(newPointerIndex);
                    mActivePointerId = ev.getPointerId(newPointerIndex);
                }
                break;
            }
        }

        return true;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save(Canvas.MATRIX_SAVE_FLAG);
        canvas.translate(mPosX, mPosY);
        canvas.scale(mScaleFactor, mScaleFactor);
        canvas.restore();
    }

    @Override
    protected void dispatchDraw(@NonNull Canvas canvas) {
        canvas.save(Canvas.MATRIX_SAVE_FLAG);
        if (mScaleFactor == 1.0f) {
            mPosX = 0.0f;
            mPosY = 0.0f;
        }
        canvas.translate(mPosX, mPosY);
        canvas.scale(mScaleFactor, mScaleFactor);
        super.dispatchDraw(canvas);
        canvas.restore();
        invalidate();
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            mScaleFactor *= detector.getScaleFactor();
            mScaleFactor = Math.max(1.0f, Math.min(mScaleFactor, 3.0f));
            maxWidth = width - (width * mScaleFactor);
            maxHeight = height - (height * mScaleFactor);
            invalidate();
            return true;
        }
    }
}

No Zoom, Clicked #34, Working No Zoom, Clicked #34, Working

Zoomed In, Clicked #34, Not Working Zoomed In, Clicked #34, Not Working

Peter Keefe
  • 992
  • 11
  • 18
  • 1
    You are manipulating with `Canvas`. Instead, you have to work with the actual layout manager, thus **managing layout process**. Currently you are just *faking* just by performing changes to the canvas, but the layout beneath it is completely not touched. Imo, you have to subclass `GridLayoutManager` and perform the heavy lifting there. – azizbekian Jun 13 '17 at 08:27
  • Do you use recyclerView.setHasFixedSize(true) ? If yes then remove that line and try – Karthik K M Jun 13 '17 at 10:31
  • See my updated answer for a gist with a little cleaner implementation. – Cheticamp Jun 13 '17 at 20:47

3 Answers3

3

Update I thought better of spreading the modifications throughout several of your classes. Here is a link to just PinchRecyclerView.java with all the changes that are needed. I will leave the entire project linked below in case there is some other use for it.


You are changing just the display on the canvas and not the underlying views as azizbekian stated in his comment. If you don't want to go with a custom GridLayoutManager and continue with the approach you have embarked upon, you will need to map screen touches back to your underlying views.

Here is your updated project on GitHub that does the mapping. Here are the basic changes:

  • Add a touch listener to the PinchRecyclerView.

  • Translate touch coordinates on the PinchRecyclerView to the corresponding position within your underlying layout.

  • Identify through searching the intended item view and do your click processing.

If you try this updated app and, as you can see in the video, the touch listener is also triggered on a pinch, so that is a bug.

Video

Kiran Benny Joseph
  • 6,271
  • 3
  • 34
  • 55
Cheticamp
  • 50,205
  • 8
  • 64
  • 109
  • Thank you @Cheticamp! It seems the key was adding an onTouchListener within the adapter. Your modifications work flawlessly. – Peter Keefe Jun 13 '17 at 21:08
  • Sorry to bring this up, @Cheticamp, I tried your code, but I am wondering why it keeps getting the selected item to the next item? For example I clicked on 38, and toast message display I clicked on 39? I have two additional views on left and right sides of recyclerview. Please help. – bEtTy Barnes May 23 '18 at 17:12
  • Thanks @Cheticamp, It is working fine for the item click, but it fails in case of row_item child click listener, If we have set the any onClickListener for any child of row_item then after scrolling or zoom in/out, it doesn't work, when we show screen first time it works as expected, but after scroll and/or zoom in/out, it doesn't work, any solution for this please ? – Jayesh Sep 29 '18 at 09:19
  • Also when we zoom the recyclerView the text we set are looks blur, is any solutions for that ? – Jayesh Sep 29 '18 at 11:51
  • Hello @Cheticamp, It would be very thankful if you have solution for above two bugs. – Jayesh Oct 12 '18 at 05:47
  • @ReazMurshed Oops. The links are reestablished. – Cheticamp Mar 03 '19 at 05:56
  • zooming functionality is working smooth. But click listener not working when it is zoom – Prateek Negi Jan 10 '20 at 10:26
0

Here is an architecture of how the recyclerview works

Adapter supplies data to > ViewHolder

ViewHolder stores the data items findviewbyid's in memory/saved location to prevent finding views every time you scroll for performance issues. Next...its

ViewHolder > LayoutManager

Layout manager stores the position of each item and does the job of updating their positions on pinch to zoom, scroll etc.

I think the way to solve this issue is to try and call

adapter.notifyDataSetChanged();

Inside a method that returns true when zoom activity is detected. Do not do this in adaper class do it in zoom activity class.

This should update the LayoutManager/ViewHolder and reassign ids.

Or

adapter.notifyItemRangeChanged(position, list.size());

To tell that item range has been changed

You can see more on this How to update RecyclerView Adapter Data?

How do I make RecyclerView update its layout?

Both Answered

Vivee Omomi
  • 158
  • 1
  • 8
  • Unfortunately, that does not work. NotifyDataSetChanged() refers to the data set that the adapter uses to populates the views but does not affect layouts in any way. Just to be sure, I tried it and it did not work, as expected. Thanks though. – Peter Keefe Jun 13 '17 at 00:05
  • If I do understand you well, do you mean that onclick listener only gets item id correctly when in normal size without zoom but returns wrong item id/object when zoom is used? I cant use my computer right now pls can you post a screenshot of the problem – Vivee Omomi Jun 13 '17 at 00:26
  • That is correct. Please see my edited question with screenshots. – Peter Keefe Jun 13 '17 at 00:34
0

Why not just add the click listeners in your RecyclerView.ViewHolder:

itemView.setOnClickListener(...);

And as azizbekian already stated, use your own LayoutManager for the layout.

artkoenig
  • 6,572
  • 1
  • 37
  • 57