am.widget.scalerecyclerview.ScaleRecyclerView.java Source code

Java tutorial

Introduction

Here is the source code for am.widget.scalerecyclerview.ScaleRecyclerView.java

Source

/*
 * Copyright (C) 2017 AlexMofer
 *
 * 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 am.widget.scalerecyclerview;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.AbsSavedState;
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 android.view.ViewParent;

import am.widget.multifunctionalrecyclerview.R;
import am.widget.scrollbarrecyclerview.ScrollbarRecyclerView;

/**
 * ?RecyclerView
 * Created by Alex on 2017/11/8.
 */
@SuppressWarnings("unused")
public class ScaleRecyclerView extends ScrollbarRecyclerView {

    public static final int SCROLL_STATE_SCALING = 3;
    @SuppressWarnings("unused")
    private static final String KEY_SCALE = "am.widget.scalerecyclerview.ScaleRecyclerView.KEY_SCALE";
    private final ScaleHelper mScaleHelper = new ScaleHelper(this);
    private final Rect tRect = new Rect();
    private boolean mScaleEnable = false;
    private GestureDetectorCompat mGestureDetector;
    private boolean mInterceptTouch = false;
    private boolean mShouldReactDoubleTab = true;
    private boolean mShouldReactSingleTab = true;
    private ScaleGestureDetector mScaleGestureDetector;
    private boolean mScaleBegin = false;
    private OnTabListener mListener;
    private float mScale;
    private float mMinScale;
    private float mMaxScale;

    public ScaleRecyclerView(Context context) {
        super(context);
        initView(context, null);
    }

    public ScaleRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView(context, attrs);
    }

    public ScaleRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView(context, attrs);
    }

    /**
     * ?
     *
     * @param child ScaleRecyclerView?
     */
    public static void setScale(View child) {
        final ViewParent parent = child.getParent();
        if (parent instanceof ScaleRecyclerView) {
            final ScaleRecyclerView view = (ScaleRecyclerView) parent;
            RecyclerView.ViewHolder holder = view.findContainingViewHolder(child);
            if (holder instanceof ViewHolder) {
                ((ViewHolder) holder).setScale(view.getScale());
            }
        }
    }

    private void initView(Context context, @Nullable AttributeSet attrs) {
        final TypedArray custom = context.obtainStyledAttributes(attrs, R.styleable.ScaleRecyclerView);
        mScaleEnable = custom.getBoolean(R.styleable.ScaleRecyclerView_srvScaleEnable, false);
        mScale = custom.getFloat(R.styleable.ScaleRecyclerView_srvScale, 1);
        mMinScale = custom.getFloat(R.styleable.ScaleRecyclerView_srvMinScale, 0.000000001f);
        mMaxScale = custom.getFloat(R.styleable.ScaleRecyclerView_srvMaxScale, 6);
        custom.recycle();
        mGestureDetector = new GestureDetectorCompat(context, new DoubleTapListener());
        mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleListener());
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!mScaleEnable)
            return super.dispatchTouchEvent(ev);
        final int action = ev.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            mScaleHelper.stop();
            mShouldReactDoubleTab = true;
            mShouldReactSingleTab = true;
        }
        final boolean superResult = super.dispatchTouchEvent(ev);
        if (!mInterceptTouch) {
            // ??
            // ?????????
            mGestureDetector.onTouchEvent(ev);
        }
        return superResult;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        if (!mScaleEnable)
            return super.onInterceptTouchEvent(e);
        mInterceptTouch = super.onInterceptTouchEvent(e);
        return mInterceptTouch;
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept) {
            mShouldReactDoubleTab = false;
            mShouldReactSingleTab = false;
        }
        super.requestDisallowInterceptTouchEvent(disallowIntercept);
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        if (!mScaleEnable)
            return super.onTouchEvent(e);
        boolean superResult = false;
        // 
        if (e.getPointerCount() >= 2) {
            // 
            if (!mScaleBegin) {
                // ACTION_UP
                final int action = e.getAction();
                e.setAction(MotionEvent.ACTION_UP);
                setForceInterceptDispatchOnScrollStateChanged(true);
                superResult = super.onTouchEvent(e);
                setForceInterceptDispatchOnScrollStateChanged(false);
                e.setAction(action);
                mScaleBegin = true;
            }
            superResult = mScaleGestureDetector.onTouchEvent(e) || superResult;
        } else {
            // ?
            final int action = e.getAction();
            boolean dispatch = false;
            if (action == MotionEvent.ACTION_UP && getScrollState() == SCROLL_STATE_IDLE) {
                dispatch = true;
            }
            if (mScaleBegin) {
                mScaleBegin = false;
                // ?ACTION_DOWN
                e.setAction(MotionEvent.ACTION_DOWN);
                super.onInterceptTouchEvent(e);
                e.setAction(action);
            }
            superResult = super.onTouchEvent(e);
            if (dispatch) {
                dispatchOnScrollStateChanged(SCROLL_STATE_IDLE);
            }
        }
        return superResult;
    }

    private void setForceInterceptDispatchOnScrollStateChanged(boolean force) {
        final ScaleLinearLayoutManager manager = getLayoutManager();
        if (manager != null) {
            manager.setForceInterceptDispatchOnScrollStateChanged(force);
        }
    }

    @Override
    public Parcelable onSaveInstanceState() {
        return new SavedState(super.onSaveInstanceState(), mScale);
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof SavedState))
            super.onRestoreInstanceState(state);
        else {
            final SavedState saved = (SavedState) state;
            super.onRestoreInstanceState(saved.getSuperState());
            mScale = saved.getScale();
            invalidateLayoutManagerScale();
            invalidate();
        }
    }

    @Override
    public ScaleLinearLayoutManager getLayoutManager() {
        final LayoutManager layoutManager = super.getLayoutManager();
        if (layoutManager == null)
            return null;
        return (ScaleLinearLayoutManager) layoutManager;
    }

    @Override
    public void setLayoutManager(LayoutManager layout) {
        if (layout != null && !(layout instanceof ScaleLinearLayoutManager))
            throw new IllegalArgumentException("Only support ScaleLinearLayoutManager.");
        super.setLayoutManager(layout);
    }

    /**
     * ?
     */
    protected void invalidateLayoutManagerScale() {
        final ScaleLinearLayoutManager manager = getLayoutManager();
        if (manager != null) {
            manager.setChildScale(mScale);
        }
    }

    /**
     * ??
     */
    protected boolean dispatchSingleTap() {
        if (mShouldReactSingleTab) {
            onSingleTap();
            return mListener != null && mListener.onSingleTap(this);
        }
        return false;
    }

    /**
     * ?
     */
    protected void onSingleTap() {
    }

    /**
     * ??
     *
     * @param e 
     */
    protected boolean dispatchDoubleTapEvent(MotionEvent e) {
        // ?ACTION_UP??
        if (e.getAction() != MotionEvent.ACTION_UP)
            return false;
        // ????View?
        if (!mShouldReactDoubleTab)
            return false;
        // ??
        onDoubleTapEvent(e);
        if (mListener != null && mListener.onDoubleTap(this))
            return true;
        // ?
        final float targetScale = getDoubleTapScale(mScale);
        if (targetScale == mScale)
            return false;
        mScaleHelper.scale(mScale, targetScale, e.getX(), e.getY());
        return true;
    }

    /**
     * ?
     *
     * @param e 
     */
    protected void onDoubleTapEvent(MotionEvent e) {
    }

    private float getDoubleTapScale(float scale) {
        final float targetScale = scale * 2;
        if (targetScale < mMaxScale) {
            return targetScale;
        }
        return mMaxScale;
    }

    /**
     * ?
     *
     * @param detector 
     * @return ??
     */
    protected boolean dispatchScaleBegin(ScaleGestureDetector detector) {
        onScaleBegin(mScale, detector.getFocusX(), detector.getFocusY());
        return true;
    }

    /**
     * 
     *
     * @param scale  
     * @param focusX X
     * @param focusY Y
     */
    protected void onScaleBegin(float scale, float focusX, float focusY) {
        dispatchOnScrollStateChanged(SCROLL_STATE_SCALING);
    }

    /**
     * ?
     *
     * @param detector 
     * @return ??
     */
    protected boolean dispatchScale(ScaleGestureDetector detector) {
        onScale(mScale * detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY());
        return true;
    }

    /**
     * ?
     *
     * @param scale  
     * @param focusX X
     * @param focusY Y
     */
    protected void onScale(float scale, float focusX, float focusY) {
        scaleTo(scale, focusX, focusY);
    }

    /**
     * 
     *
     * @param scale  
     * @param focusX X
     * @param focusY Y
     */
    public void scaleTo(float scale, float focusX, float focusY) {
        scale = scale > mMaxScale ? mMaxScale : scale;
        scale = scale < mMinScale ? mMinScale : scale;
        if (scale == mScale)
            return;
        final ScaleLinearLayoutManager manager = getLayoutManager();
        if (manager == null) {
            mScale = scale;
            invalidateLayoutManagerScale();
            requestLayout();
            return;
        }
        final View target = findChildViewNear(focusX, focusY);
        if (target == null) {
            mScale = scale;
            invalidateLayoutManagerScale();
            requestLayout();
            return;
        }
        final int position = getChildAdapterPosition(target);
        float maxWidth = manager.getChildMaxWidth(manager.getChildMaxWidth());
        float maxHeight = manager.getChildMaxHeight(manager.getChildMaxHeight());
        final int offsetA = manager.computeAnotherDirectionScrollOffset();
        final float normalWidth = target.getWidth() / mScale;
        final float normalHeight = target.getHeight() / mScale;
        final float inset;
        final float focusA;
        final float focusS;
        getDecoratedBoundsWithMargins(target, tRect);
        if (manager.getOrientation() == HORIZONTAL) {
            focusA = (offsetA + (focusY - getPaddingTop())) / maxHeight;
            inset = target.getLeft() - tRect.left;
            focusS = (focusX - target.getLeft()) / target.getWidth();
        } else {
            focusA = (offsetA + (focusX - getPaddingLeft())) / maxWidth;
            focusS = (focusY - target.getTop()) / target.getHeight();
            inset = target.getTop() - tRect.top;
        }
        mScale = scale;
        invalidateLayoutManagerScale();
        final float maxOffset = manager.computeAnotherDirectionMaxScrollOffset();
        if (maxOffset <= 0) {
            manager.setAnotherDirectionScrollOffsetPercentage(0);
        } else {
            maxWidth = manager.getChildMaxWidth(manager.getChildMaxWidth());
            maxHeight = manager.getChildMaxHeight(manager.getChildMaxHeight());
            final float scaleOffset;
            if (manager.getOrientation() == HORIZONTAL) {
                scaleOffset = focusA * maxHeight - (focusY - getPaddingTop());
            } else {
                scaleOffset = focusA * maxWidth - (focusX - getPaddingLeft());
            }
            manager.setAnotherDirectionScrollOffsetPercentage(scaleOffset / maxOffset);
        }
        final float offsetS;
        if (manager.getOrientation() == HORIZONTAL) {
            offsetS = -(focusS * normalWidth * mScale - (focusX - getPaddingLeft())) - inset;
        } else {
            offsetS = -(focusS * normalHeight * mScale - (focusY - getPaddingTop())) - inset;
        }
        manager.scrollToPositionWithOffset(position, Math.round(offsetS));
    }

    /**
     * ??
     *
     * @param detector 
     */
    protected void dispatchScaleEnd(ScaleGestureDetector detector) {
        onScaleEnd(mScale, detector.getFocusX(), detector.getFocusY());
    }

    /**
     * ??
     *
     * @param scale  
     * @param focusX X
     * @param focusY Y
     */
    protected void onScaleEnd(float scale, float focusX, float focusY) {
        if (mScaleBegin)
            return;
        dispatchOnScrollStateChanged(SCROLL_STATE_IDLE);
    }

    /**
     * ?
     *
     * @param scale  
     * @param focusX X
     * @param focusY Y
     */
    protected void onDoubleTapScaleBegin(float scale, float focusX, float focusY) {
        dispatchOnScrollStateChanged(SCROLL_STATE_SCALING);
    }

    /**
     * ?
     *
     * @param scale  
     * @param focusX X
     * @param focusY Y
     */
    protected void onDoubleTapScale(float scale, float focusX, float focusY) {
        scaleTo(scale, focusX, focusY);
    }

    /**
     * ??
     *
     * @param scale  
     * @param focusX X
     * @param focusY Y
     */
    protected void onDoubleTapScaleEnd(float scale, float focusX, float focusY) {
        if (mScaleBegin)
            return;
        dispatchOnScrollStateChanged(SCROLL_STATE_IDLE);
    }

    /**
     * ??
     *
     * @return ??
     */
    public boolean isScaleEnable() {
        return mScaleEnable;
    }

    /**
     * ??
     *
     * @param enable ??
     */
    public void setScaleEnable(boolean enable) {
        mScaleEnable = enable;
    }

    /**
     * ???
     *
     * @param listener ?
     */
    public void setOnTabListener(OnTabListener listener) {
        mListener = listener;
    }

    /**
     * 
     *
     * @param min ?
     * @param max 
     */
    public void setScaleRange(float min, float max) {
        mMinScale = min;
        mMaxScale = max;
    }

    /**
     * ?
     *
     * @return 
     */
    public float getScale() {
        return mScale;
    }

    /**
     * 
     *
     * @param scale 
     */
    public void setScale(float scale) {
        if (scale < mMinScale || scale > mMaxScale || scale == mScale)
            return;
        mScale = scale;
        invalidateLayoutManagerScale();
        requestLayout();
    }

    /**
     * ???
     */
    public interface OnTabListener {
        /**
         * ?
         *
         * @param view ScaleRecyclerView
         * @return ?
         */
        boolean onSingleTap(ScaleRecyclerView view);

        /**
         * ?
         *
         * @param view ScaleRecyclerView
         * @return ?
         */
        boolean onDoubleTap(ScaleRecyclerView view);
    }

    /**
     * ??
     *
     * @param <VH> ?
     */
    public static abstract class Adapter<VH extends ViewHolder> extends RecyclerView.Adapter<VH> {

        private ScaleRecyclerView mView;

        @Override
        public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
            if (recyclerView instanceof ScaleRecyclerView) {
                mView = (ScaleRecyclerView) recyclerView;
            }
            super.onAttachedToRecyclerView(recyclerView);
        }

        @Override
        public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
            mView = null;
            super.onDetachedFromRecyclerView(recyclerView);
        }

        /**
         * ?
         *
         * @return 
         */
        public float getScale() {
            return mView == null ? 1 : mView.getScale();
        }
    }

    /**
     * ?
     */
    public static class ViewHolder extends RecyclerView.ViewHolder {
        public ViewHolder(View itemView) {
            super(itemView);
        }

        /**
         * 
         *
         * @param scale 
         */
        public void setScale(float scale) {

        }
    }

    private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            return dispatchSingleTap();
        }

        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            return dispatchDoubleTapEvent(e);
        }
    }

    private class ScaleListener implements ScaleGestureDetector.OnScaleGestureListener {

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            return dispatchScaleBegin(detector);
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            return dispatchScale(detector);
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            dispatchScaleEnd(detector);
        }
    }

    protected static class SavedState extends AbsSavedState {

        public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {

            @Override
            public SavedState createFromParcel(Parcel source, ClassLoader loader) {
                return new SavedState(source, loader);
            }

            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in, null);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };

        private final float mScale;

        private SavedState(Parcel in, ClassLoader loader) {
            super(in, loader);
            mScale = in.readFloat();
        }

        private SavedState(Parcelable superState, float scale) {
            super(superState);
            mScale = scale;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeFloat(mScale);
        }

        float getScale() {
            return mScale;
        }
    }
}