Java tutorial
/* * 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; } } }