io.apptik.multiview.scalablerecyclerview.ScalableRecyclerGridView.java Source code

Java tutorial

Introduction

Here is the source code for io.apptik.multiview.scalablerecyclerview.ScalableRecyclerGridView.java

Source

/*
 * Copyright (C) 2015 AppTik Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.apptik.multiview.scalablerecyclerview;

import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.support.v4.view.GestureDetectorCompat;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;

import io.apptik.multiview.common.Log;
import io.apptik.multiview.layoutmanagers.ScalableGridLayoutManager;
import io.apptik.multiview.scrollers.BaseSmoothScroller;
import io.apptik.multiview.scrollers.FlexiSmoothScroller;

/**
 * Scalable Recyclerview that accept only gridviewlayoutmanager
 */

public class ScalableRecyclerGridView extends RecyclerView {

    private int minSpan = 2;
    private int maxSpan = 5;

    InteractionListener interactionListener;

    public static final int ZOOM_ANIMATION_DURATION_MS = 200;
    public ScaleListener scaleListener;

    public ScalableRecyclerGridView(Context context, int intialSpan) {
        super(context);
        init(context, intialSpan);
    }

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

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

    private void init(Context context, int initSpanCount) {
        super.setLayoutManager(new ScalableGridLayoutManager(context, initSpanCount));
        interactionListener = new InteractionListener(context, this);
    }

    public ScaleListener getScaleListener() {
        return scaleListener;
    }

    public void setScaleListener(ScaleListener scaleListener) {
        this.scaleListener = scaleListener;
    }

    public int getMaxSpan() {
        return maxSpan;
    }

    public void setMaxSpan(int mMaxSpan) {
        this.maxSpan = mMaxSpan;
        if (((ScalableGridLayoutManager) getLayoutManager()) != null
                && ((ScalableGridLayoutManager) getLayoutManager()).getSpanCount() > mMaxSpan) {
            ((ScalableGridLayoutManager) getLayoutManager()).setSpanCount(mMaxSpan);
        }
    }

    public int getMinSpan() {
        return minSpan;
    }

    public void setMinSpan(int mMinSpan) {
        this.minSpan = mMinSpan;
        if (((ScalableGridLayoutManager) getLayoutManager()) != null
                && ((ScalableGridLayoutManager) getLayoutManager()).getSpanCount() < mMinSpan) {
            ((ScalableGridLayoutManager) getLayoutManager()).setSpanCount(mMinSpan);
        }
    }

    public void setSpanCount(int spanCount) {
        if (spanCount < minSpan) {
            spanCount = minSpan;
        }
        if (spanCount > maxSpan) {
            spanCount = maxSpan;
        }
        int oldSpanCount = ((ScalableGridLayoutManager) getLayoutManager()).getSpanCount();
        if (oldSpanCount != spanCount) {
            ((ScalableGridLayoutManager) getLayoutManager()).setSpanCount(spanCount);
            if (scaleListener != null) {
                scaleListener.onSpanChange(spanCount, oldSpanCount);
            }
        }
    }

    @Override
    public void setLayoutManager(LayoutManager layout) {
        if (layout.getClass().isAssignableFrom(ScalableGridLayoutManager.class)) {
            throw new IllegalStateException("LAyoutManager has to extend ScalableGridLayoutManager ");
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean isGood = interactionListener.onTouchEvent(ev);

        if (!isGood) {
            return super.onTouchEvent(ev);
        }

        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!isAttachedToWindow() || getAdapter() == null || getLayoutManager() == null
        //||getScrollState() != SCROLL_STATE_IDLE
        ) {
            return super.onInterceptTouchEvent(ev);
        } else {
            onTouchEvent(ev);
        }

        return false;
    }

    @Override
    public void onDraw(Canvas c) {
        if (interactionListener.currentView != null && interactionListener.scaleGestureDetector.isInProgress()) {
            float px;
            float py;

            //pivot for zooming depends on the ratio of the distance of left and
            // right(top and bottom) of the child to left and right(top and bottom) of the RV
            float dl = interactionListener.currentView.getX() - getX();
            float dw = getWidth() - interactionListener.currentView.getWidth();
            px = interactionListener.currentView.getX() + (dl / dw) * interactionListener.currentView.getWidth();
            float dt = interactionListener.currentView.getY() - getY();
            float dh = getHeight() - interactionListener.currentView.getHeight();
            py = interactionListener.currentView.getY() + (dt / dh) * interactionListener.currentView.getHeight();
            //however we also do not want to have half appearing views so we choose a side
            float midX = getX() + getWidth() / 2;
            float midY = getY() + getHeight() / 2;
            if (getLayoutManager().canScrollHorizontally()) {
                //fix py
                if (py < midY) {
                    py = 0;
                } else {
                    py = getY() + getHeight();
                }
            } else {
                //fix px
                if (px < midX) {
                    px = 0;
                } else {
                    px = getX() + getWidth();
                }
            }

            c.scale(getCurrScale(), getCurrScale(), px, py);
        }
        super.onDraw(c);
    }

    private float getCurrScale() {
        return interactionListener.currScale;
    }

    @Override
    public void scrollBy(int x, int y) {
        super.scrollBy(x, y);
    }

    public static class InteractionListener implements GestureDetector.OnGestureListener,
            GestureDetector.OnDoubleTapListener, ScaleGestureDetector.OnScaleGestureListener {

        private Context context;
        private ScalableRecyclerGridView scalableRecyclerGridView;
        private GestureDetectorCompat gestureDetector;
        private ScaleGestureDetector scaleGestureDetector;
        private float currScale = 1f;
        //initial span count when scale started
        volatile int initSpanCount = 0;
        volatile View currentView;
        //used to normalise the raw factor when scaling in not allowed direction
        volatile private float factorOffset;

        public InteractionListener(Context context, ScalableRecyclerGridView scalableRecyclerGridView) {
            this.context = context;
            this.scalableRecyclerGridView = scalableRecyclerGridView;
            init();
        }

        private void init() {
            gestureDetector = new GestureDetectorCompat(context, this);
            gestureDetector.setOnDoubleTapListener(this);
            scaleGestureDetector = new ScaleGestureDetector(context, this);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                scaleGestureDetector.setQuickScaleEnabled(false);
            }
        }

        public boolean onTouchEvent(MotionEvent ev) {
            scaleGestureDetector.onTouchEvent(ev);
            //if were in scale ignore the rest
            if (scaleGestureDetector.isInProgress()) {
                return true;
            } else {
                //return false;
                return gestureDetector.onTouchEvent(ev);
            }

        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            Log.d("onSingleTapConfirmed: " + ((e == null) ? "e==null" : e.getX() + "/" + e.getY()));
            if (scaleGestureDetector.isInProgress())
                return true;
            return false;
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            Log.d("onDoubleTap: " + ((e == null) ? "e==null" : e.getX() + "/" + e.getY()) + " v: " + currentView);
            if (scaleGestureDetector.isInProgress())
                return false;
            //toggle min/max col span for grid mode

            View tmpView = scalableRecyclerGridView.findChildViewUnder(e.getX(), e.getY());

            if (((ScalableGridLayoutManager) scalableRecyclerGridView.getLayoutManager())
                    .getSpanCount() > scalableRecyclerGridView.getMinSpan()) {
                scalableRecyclerGridView.setSpanCount(scalableRecyclerGridView.getMinSpan());
            } else {
                scalableRecyclerGridView.setSpanCount(scalableRecyclerGridView.getMaxSpan());
            }
            if (tmpView != null) {
                scalableRecyclerGridView
                        .scrollToPosition(scalableRecyclerGridView.getChildAdapterPosition(tmpView));
            }
            scalableRecyclerGridView.requestLayout();

            return true;
        }

        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            Log.d("onDoubleTapEvent: " + ((e == null) ? "e==null" : e.getX() + "/" + e.getY()));
            if (scaleGestureDetector.isInProgress())
                return false;
            //TODO do we ?
            //not in use now but we have to return true
            return false;
        }

        @Override
        public boolean onDown(MotionEvent e) {
            Log.d("onDown: " + ((e == null) ? "e==null" : e.getX() + "/" + e.getY()));
            return false;
        }

        @Override
        public void onShowPress(MotionEvent e) {
            Log.d("onShowPress: " + ((e == null) ? "e==null" : e.getX() + "/" + e.getY()));
            //just ignore
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            Log.d("onSingleTapUp: " + ((e == null) ? "e==null" : e.getX() + "/" + e.getY()));
            if (scaleGestureDetector.isInProgress())
                return true;
            return false;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            Log.d("onScroll: " + ((e1 == null) ? "e1==null" : e1.getX() + "/" + e1.getY()) + " - "
                    + ((e2 == null) ? "e2==null" : e2.getX() + "/" + e2.getY()) + " :: " + distanceX + "/"
                    + distanceY);
            if (scaleGestureDetector.isInProgress())
                return true;
            return false;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            Log.d("onFling: " + ((e1 == null) ? "e1==null" : e1.getX() + "/" + e1.getY()) + " - "
                    + ((e2 == null) ? "e2==null" : e2.getX() + "/" + e2.getY()) + " :: " + velocityX + "/"
                    + velocityY);
            if (scaleGestureDetector.isInProgress())
                return true;
            return false;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            Log.d("onLongPress: " + ((e == null) ? "e==null" : e.getX() + "/" + e.getY()));
            //maybe menu
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            Log.d("onScale: " + detector.getFocusX() + "/" + detector.getFocusY() + " : "
                    + detector.getCurrentSpan() + "/" + detector.getScaleFactor() + "/" + factorOffset + " : "
                    + detector.getEventTime());

            float currFactor = detector.getScaleFactor() + factorOffset;

            int newSpanCount = ((ScalableGridLayoutManager) scalableRecyclerGridView.getLayoutManager())
                    .getSpanCount();
            if ((initSpanCount == newSpanCount)
                    && ((currFactor > 1 && newSpanCount == scalableRecyclerGridView.getMinSpan()) || (currFactor < 1
                            && currScale <= 1 && newSpanCount == scalableRecyclerGridView.getMaxSpan()))) {
                factorOffset = 1 - detector.getScaleFactor();
                return false;
            }

            float newScale = currFactor;

            if (currFactor > 1.05f) {
                //zoomin
                //check if we did zoom out before
                if (newSpanCount > initSpanCount) {
                    newSpanCount = initSpanCount;
                }
                Log.d("onScale: zoomin " + newSpanCount);
                if (initSpanCount == 1) {
                    //change to single LM here
                    //setSingleMode();
                    //newScale = 1;
                } else {
                    newScale = Math.min(currFactor, (float) (initSpanCount) / (float) (initSpanCount - 1));
                }

            } else if (currFactor < 0.95) {
                //zoomout
                newSpanCount = initSpanCount + 1;
                Log.d("onScale: zoomout " + newSpanCount);
                newScale = 1f + currFactor - (float) initSpanCount / (float) newSpanCount;
                newScale = Math.max(1f, newScale);
            } else {
                //in between dont change span count
                //check if we were zoomed out before so we have overflowing items
                if (newSpanCount > initSpanCount) {
                    newScale = 1f + currFactor - (float) initSpanCount / (float) newSpanCount;
                    newScale = Math.max(1f, newScale);
                } else {
                    if (initSpanCount == 1) {

                    } else {
                        newScale = Math.min(currFactor, (float) (initSpanCount) / (float) (initSpanCount - 1));
                    }
                }
            }
            if (newSpanCount != ((ScalableGridLayoutManager) scalableRecyclerGridView.getLayoutManager())
                    .getSpanCount()) {
                Log.d("onScale: will set " + newSpanCount);
                scalableRecyclerGridView.setSpanCount(newSpanCount);
                scalableRecyclerGridView.getLayoutManager().requestLayout();
                if (initSpanCount != ((ScalableGridLayoutManager) scalableRecyclerGridView.getLayoutManager())
                        .getSpanCount()) {
                    SmoothScroller scroller = new FlexiSmoothScroller(context)
                            .setVerticalSnapPreference(BaseSmoothScroller.SNAP_TO_CENTER)
                            .setHorizontalSnapPreference(BaseSmoothScroller.SNAP_TO_CENTER);
                    scroller.setTargetPosition(scalableRecyclerGridView.getChildAdapterPosition(currentView));
                    scalableRecyclerGridView.getLayoutManager().startSmoothScroll(scroller);
                }
            }
            currScale = newScale;
            scalableRecyclerGridView.invalidate();

            return false;
        }

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            Log.d("onScaleBegin: " + detector.getFocusX() + "/" + detector.getFocusY() + " : "
                    + detector.getCurrentSpan() + "/" + detector.getScaleFactor() + " : "
                    + detector.getEventTime());
            currentView = scalableRecyclerGridView.findChildViewUnder(detector.getFocusX(), detector.getFocusY());
            initSpanCount = ((ScalableGridLayoutManager) scalableRecyclerGridView.getLayoutManager())
                    .getSpanCount();
            factorOffset = 0f;
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            Log.d("onScaleEnd: " + detector.getFocusX() + "/" + detector.getFocusY() + " : "
                    + detector.getCurrentSpan() + "/" + detector.getScaleFactor() + "/" + factorOffset + " : "
                    + detector.getEventTime());

            float currFactor = detector.getScaleFactor() + factorOffset;

            if (currFactor > 1f + (1f / (float) (initSpanCount - 1)) / 2f) {
                if (initSpanCount == scalableRecyclerGridView.minSpan) {
                    //do nothing we reached our max span
                } else {
                    scalableRecyclerGridView.setSpanCount(initSpanCount - 1);
                }
            } else if (currFactor < 1f - (1f / (float) (initSpanCount + 1)) / 2f) {
                scalableRecyclerGridView.setSpanCount(initSpanCount + 1);
            } else {
                if (((ScalableGridLayoutManager) scalableRecyclerGridView.getLayoutManager())
                        .getSpanCount() != initSpanCount) {
                    scalableRecyclerGridView.setSpanCount(initSpanCount);
                }
            }

            if (initSpanCount != ((ScalableGridLayoutManager) scalableRecyclerGridView.getLayoutManager())
                    .getSpanCount()) {
                scalableRecyclerGridView.getLayoutManager()
                        .scrollToPosition(scalableRecyclerGridView.getChildAdapterPosition(currentView));
            }
            currScale = 1f;
            initSpanCount = 0;
            scalableRecyclerGridView.getLayoutManager().requestLayout();
            scalableRecyclerGridView.invalidate();

            currentView = null;
        }

    }

    public interface ScaleListener {
        void onScaleBegin(ScaleGestureDetector detector);

        void onScaleEnd(ScaleGestureDetector detector);

        void onScale(ScaleGestureDetector detector);

        void onSpanChange(int newSpan, int oldSpan);
    }

    public static class VoidScaleListener implements ScaleListener {

        @Override
        public void onScaleBegin(ScaleGestureDetector detector) {

        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {

        }

        @Override
        public void onScale(ScaleGestureDetector detector) {

        }

        @Override
        public void onSpanChange(int newSpan, int oldSpan) {

        }

    }

}