ViewPicker
package com.xunrui.camera.ui.commonui.viewpicker;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.widget.OverScroller;
import androidx.core.view.ViewCompat;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FloatValueHolder;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.xunrui.camera.R;
public class ViewPicker extends ViewGroup implements View.OnClickListener {
private static final float INFLEXION = 0.35f;
private static final float mPhysicalCoeff = 9.80665f * 39.37f * 1 * 160 * 0.84f;
private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
private static final float OVER_DRAGGER_FRICTION = 0.2f;
private static final float OVER_DRAGGER_DAMPING_RATIO = 1f;
private static final float OVER_DRAGGER_STIFFNESS = 200f;
static final double STANDARD_VAL = 1.0704514448933802d;
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
private static final int SCROLL_STATUS_IDLE = 0;
private static final int SCROLL_STATUS_TOUCH_DRAG = 1;
private static final int SCROLL_STATUS_OVER_DRAG = 2;
private static final int SCROLL_STATUS_INERTIA = 3;
private static final int SCROLL_STATUS_SNAP = 4;
private static final int SCROLL_STATUS_SMOOTH = 5;
private static int VELOCITY_UNIT = 200;
private static float SCROLL_FRICTION = 0.005F;
private static float MILLISECONDS_PER_INCH = 100f;
private static float SMOOTH_PARAM = 300f;
private static int SCALED_TOUCH_SLOP;
private final int ITEM_MEASURE_UNSPECIFIED_SPEC = MeasureSpec.makeMeasureSpec(4000, MeasureSpec.UNSPECIFIED);
private final int ITEM_MEASURE_AT_MOST_SPEC = MeasureSpec.makeMeasureSpec(4000, MeasureSpec.AT_MOST);
private float mCenterScale;
private int mDivider;
private final int mOverDragLen = 69;
private int mOrientation;
private float mDownX;
private float mDownY;
private int mScrollWhenDown;
private int mLastMoveX;
private int mLastMoveY;
private final int mMinScroll = 0;
private int mMaxScroll;
private int[] mChildNormalSize = new int[0];
private int[] mChildFocusSize = new int[0];
private int mMaxItemSize = 0;
private final SparseArray<Float> mScaleArray = new SparseArray<>(2);
private int mFocusViewIndex = 0;
private float mFocusWidth = 0;
private float mFocusHeight = 0;
private VelocityTracker mVelocityTracker;
private int mMinFlingVelocity;
private int mMaxFlingVelocity;
private int mScrollPointerId;
private boolean sIsInterrupt = false;
private int mStatus = SCROLL_STATUS_IDLE;
private boolean mIsClickedPicker = false;
private boolean mIsClickingItem = false;
private boolean mIsItemChanged = true;
private int mFocusIndex;
private int mLastFocusIndex;
private ViewPickerAdapter mViewPickerAdapter = DEFAULT_ADAPTER;
private ViewFlinger mViewFlinger = new ViewFlinger();
private SpringAnimation mOverDragRecoverAnim;
private OnViewPickerListener mOnViewPickerListener = DEFAULT_LISTENER;
private static final ViewPickerAdapter DEFAULT_ADAPTER = new ViewPickerAdapter();
private static final OnViewPickerListener DEFAULT_LISTENER = new OnViewPickerListener() {
};
public ViewPicker(Context context) {
this(context, null);
}
public ViewPicker(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ViewPicker(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public ViewPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
initParams(context);
}
private void init(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ViewPicker);
mCenterScale = ta.getFloat(R.styleable.ViewPicker_centerScale, 1f);
mDivider = ta.getDimensionPixelSize(R.styleable.ViewPicker_itemDivider, 0);
mOrientation = ta.getInt(R.styleable.ViewPicker_android_orientation, VERTICAL);
ta.recycle();
}
private void initParams(Context context) {
mVelocityTracker = VelocityTracker.obtain();
ViewConfiguration vc = ViewConfiguration.get(context);
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
SCALED_TOUCH_SLOP = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
public void setScrollConstByRecyclerView() {
VELOCITY_UNIT = 1000;
SCROLL_FRICTION = 0.015f;
MILLISECONDS_PER_INCH = 100;
SMOOTH_PARAM = 100;
mMinFlingVelocity = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity();
mViewFlinger = new ViewFlinger();
}
public void setScrollConstSlowly() {
VELOCITY_UNIT = 200;
SCROLL_FRICTION = 0.005f;
MILLISECONDS_PER_INCH = 100;
SMOOTH_PARAM = 300;
mMinFlingVelocity = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity();
mViewFlinger = new ViewFlinger();
}
public void setScrollConstQuickly() {
VELOCITY_UNIT = 800;
SCROLL_FRICTION = 1f;
MILLISECONDS_PER_INCH = 10;
SMOOTH_PARAM = 30;
mMinFlingVelocity = getMinVelocity(SCROLL_FRICTION);
mViewFlinger = new ViewFlinger();
}
private static int getMinVelocity(float friction) {
double decelMinusOne = DECELERATION_RATE - 1.0;
return (int) Math.ceil(Math.exp(Math.log(STANDARD_VAL / (friction * mPhysicalCoeff))
* decelMinusOne / DECELERATION_RATE) * (friction * mPhysicalCoeff) / INFLEXION);
}
public void setAdapter(ViewPickerAdapter adapter) {
if (adapter == null) {
adapter = DEFAULT_ADAPTER;
}
mViewPickerAdapter = adapter;
setWillNotDraw(mViewPickerAdapter.willNotDraw());
adapter.registerDataSetObserver(new DataSetObserver() {
@Override
public void onChanged() {
mIsItemChanged = true;
addItems();
measureStandardChildSize();
requestLayout();
}
});
adapter.notifyDataSetChanged();
}
private void measureStandardChildSize() {
int widthMeasureSpec = ITEM_MEASURE_AT_MOST_SPEC;
int heightMeasureSpec = ITEM_MEASURE_UNSPECIFIED_SPEC;
if (isHorizontal()) {
widthMeasureSpec = ITEM_MEASURE_UNSPECIFIED_SPEC;
heightMeasureSpec = ITEM_MEASURE_AT_MOST_SPEC;
}
int childCount = getChildCount();
View child;
int[] measureWidths = new int[childCount];
int[] measureHeights = new int[childCount];
setAllItemNormalStyle();
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
mViewPickerAdapter.setNormalStyle(child);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
measureWidths[i] = child.getMeasuredWidth();
measureHeights[i] = child.getMeasuredHeight();
}
mChildFocusSize = mChildNormalSize = isHorizontal() ? measureWidths : measureHeights;
if (!mViewPickerAdapter.isCenterSizeSameAsNormal()) {
measureWidths = new int[childCount];
measureHeights = new int[childCount];
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
mViewPickerAdapter.setFocusStyle(child);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
measureWidths[i] = child.getMeasuredWidth();
measureHeights[i] = child.getMeasuredHeight();
mViewPickerAdapter.setNormalStyle(child);
}
mChildFocusSize = isHorizontal() ? measureWidths : measureHeights;
}
mMaxScroll = getScrollByFocusIndex(childCount - 1);
mMaxItemSize = 0;
int[] arr = isHorizontal() ? measureHeights : measureWidths;
for (int size : arr) {
mMaxItemSize = Math.max(mMaxItemSize, size);
}
}
private void setAllItemNormalStyle() {
int childCount = getChildCount();
View child;
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
mViewPickerAdapter.setNormalStyle(child);
}
}
private void addItems() {
View item;
for (int i = 0; i < mViewPickerAdapter.getCount(); i++) {
item = mViewPickerAdapter.getView(i, null, this);
item.setTag(i);
item.setOnClickListener(this);
addView(item);
}
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
public void onClick(View v) {
if (!mIsClickingItem) {
mIsClickingItem = true;
focusItem((Integer) v.getTag());
}
}
public void focusItem(int index) {
if (index < 0 || index >= mViewPickerAdapter.getCount()) {
throw new IndexOutOfBoundsException("initCenter width invalid index: " + index);
}
focusItem(index, true);
}
public void focusItemWidthOutSmoothScroll(int index) {
if (index < 0 || index >= mViewPickerAdapter.getCount()) {
throw new IndexOutOfBoundsException("initCenter width invalid index: " + index);
}
if (!mIsItemChanged) {
focusItem(index, false);
return;
}
focusChanged(index, true);
requestLayout();
}
private void focusItem(int index, boolean hasSmoothScroll) {
if (hasSmoothScroll && index != mFocusIndex) {
focusStateChanged(false);
}
int scroll = getScroll();
int targetLen = getScrollByFocusIndex(index);
if (hasSmoothScroll) {
mViewFlinger.smoothScrollBy(targetLen - scroll);
} else {
if (isHorizontal()) {
scrollBy(targetLen - scroll, 0);
} else {
scrollBy(0, targetLen - scroll);
}
focusChanged(index, true);
}
}
private void focusChanged(int index, boolean isInit) {
mLastFocusIndex = mFocusIndex;
mFocusIndex = index;
mIsClickingItem = false;
focusStateChanged(true);
if (mOnViewPickerListener != null && (isInit || mLastFocusIndex != mFocusIndex)) {
mOnViewPickerListener.onSelected(mFocusIndex, getChildAt(mFocusIndex));
}
}
private void focusStateChanged(boolean isInFocus) {
mViewPickerAdapter.focusStateChanged(isInFocus, getChildAt(mFocusIndex), getChildAt(mLastFocusIndex));
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mIsClickingItem || mIsItemChanged) {
return false;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean isHandle = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mStatus = SCROLL_STATUS_IDLE;
sIsInterrupt = false;
mViewFlinger.cancelAnimation();
if (mOverDragRecoverAnim != null && mOverDragRecoverAnim.isRunning()) {
mOverDragRecoverAnim.cancel();
}
mScrollPointerId = event.getPointerId(0);
mLastMoveX = (int) (mDownX = event.getX());
mLastMoveY = (int) (mDownY = event.getY());
mScrollWhenDown = getScroll();
mIsClickedPicker = true;
break;
case MotionEvent.ACTION_MOVE:
if (sIsInterrupt || isTriggerScroll(event)) {
sIsInterrupt = true;
isHandle = true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsClickedPicker = false;
if (sIsInterrupt) {
isHandle = true;
}
break;
}
return isHandle || super.onInterceptTouchEvent(event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int move;
int x = (int) event.getX();
int y = (int) event.getY();
move = isHorizontal() ? mLastMoveX - x : mLastMoveY - y;
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
if (mIsClickedPicker && isTriggerScroll(event)) {
focusStateChanged(false);
mIsClickedPicker = false;
}
int scroll = getScroll() + move;
int realScroll = (int) (mScrollWhenDown + (isHorizontal() ? mDownX - x : mDownY - y));
if (scroll < mMinScroll) {
mStatus = SCROLL_STATUS_OVER_DRAG;
int target = -calculateSpringPressDistance(mMinScroll - realScroll);
move = target - (getScroll() - mMinScroll);
} else if (scroll > mMaxScroll) {
mStatus = SCROLL_STATUS_OVER_DRAG;
int target = calculateSpringPressDistance(realScroll - mMaxScroll);
move = target - (getScroll() - mMaxScroll);
} else {
mStatus = SCROLL_STATUS_TOUCH_DRAG;
}
move(move);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mStatus == SCROLL_STATUS_OVER_DRAG) {
overDrag();
break;
}
mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT, mMaxFlingVelocity);
float vel;
if (isHorizontal()) {
vel = -mVelocityTracker.getXVelocity(mScrollPointerId);
} else {
vel = -mVelocityTracker.getYVelocity(mScrollPointerId);
}
if (!fling((int) vel)) {
if (!snap(move)) {
mStatus = SCROLL_STATUS_IDLE;
getFocusIndexByScroll();
}
}
break;
}
mLastMoveX = x;
mLastMoveY = y;
return true;
}
private void getFocusIndexByScroll() {
int scroll = getScroll();
if (scroll == 0) {
focusChanged(0, false);
return;
}
int len = mChildNormalSize[0] - mChildNormalSize[0] / 2;
int childCount = getChildCount();
int target;
for (int i = 1; i < childCount; i++) {
target = len + mDivider + Math.round(mChildFocusSize[i] * mCenterScale) / 2;
if (target == scroll) {
focusChanged(i, false);
break;
}
if (target > scroll) {
return;
}
len += mDivider + mChildNormalSize[i];
}
}
private int getScrollByFocusIndex(int index) {
if (index == 0 || getChildCount() == 0) {
return 0;
}
int scroll = mChildNormalSize[0] - mChildNormalSize[0] / 2;
for (int i = 1; i < index; i++) {
scroll += mDivider + mChildNormalSize[i];
}
scroll += mDivider + Math.round(mChildFocusSize[index] * mCenterScale) / 2;
return scroll;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
calculateChildrenScales();
int childCount = mViewPickerAdapter.getCount();
if (mIsItemChanged) {
mIsItemChanged = false;
}
if (!mViewPickerAdapter.isCenterSizeSameAsNormal()) {
if (isHorizontal()) {
measureChildren(ITEM_MEASURE_UNSPECIFIED_SPEC, heightMeasureSpec);
} else {
measureChildren(widthMeasureSpec, ITEM_MEASURE_UNSPECIFIED_SPEC);
}
}
correctScroll();
getCurrentFocusSize();
float scale;
if (isHorizontal()) {
for (int i = 0; i < childCount; i++) {
scale = mScaleArray.get(i, -1f);
if (scale < 0) {
mViewPickerAdapter.childScale(getChildAt(i), 1f);
} else {
mViewPickerAdapter.childScale(getChildAt(i), scale);
}
}
} else {
for (int i = 0; i < childCount; i++) {
scale = mScaleArray.get(i, -1f);
if (scale < 0) {
mViewPickerAdapter.childScale(getChildAt(i), 1f);
} else {
mViewPickerAdapter.childScale(getChildAt(i), scale);
}
}
}
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
int maxItemSize = (int) Math.ceil(mMaxItemSize * mCenterScale);
if (isHorizontal()) {
if (hMode == MeasureSpec.AT_MOST) {
height = Math.min(maxItemSize, height);
}
if (hMode == MeasureSpec.UNSPECIFIED) {
height = maxItemSize;
}
} else {
if (wMode == MeasureSpec.AT_MOST) {
width = Math.min(maxItemSize, width);
}
if (wMode == MeasureSpec.UNSPECIFIED) {
width = maxItemSize;
}
}
setMeasuredDimension(width, height);
}
private void getCurrentFocusSize() {
View view = getChildAt(mFocusViewIndex);
if (view == null) {
return;
}
float scale = mScaleArray.get(mFocusViewIndex);
mFocusWidth = view.getMeasuredWidth() * scale;
mFocusHeight = view.getMeasuredHeight() * scale;
}
private void correctScroll() {
if (mStatus != SCROLL_STATUS_IDLE) {
return;
}
int scroll = getScroll();
int targetLen = getScrollByFocusIndex(mFocusIndex);
if (targetLen == scroll) {
return;
}
if (isHorizontal()) {
scrollBy(targetLen - scroll, 0);
} else {
scrollBy(0, targetLen - scroll);
}
}
private void calculateChildrenScales() {
int scroll = getScroll();
mScaleArray.clear();
setAllItemNormalStyle();
if (scroll == 0) {
mFocusViewIndex = 0;
mScaleArray.put(0, mCenterScale);
mViewPickerAdapter.setFocusStyle(getChildAt(0));
return;
}
int len = mChildNormalSize[0] - mChildNormalSize[0] / 2;
int childCount = getChildCount();
int target;
boolean isInFocus = false;
int index = 0;
for (int i = 1; i < childCount; i++) {
index = i;
target = len + mDivider + Math.round(mChildFocusSize[i] * mCenterScale) / 2;
if (target == scroll) {
isInFocus = true;
break;
}
if (target > scroll) {
break;
}
len += mDivider + mChildNormalSize[i];
}
if (isInFocus) {
mFocusViewIndex = index;
mScaleArray.put(index, mCenterScale);
mViewPickerAdapter.setFocusStyle(getChildAt(index));
return;
}
int indexFocusLen = getScrollByFocusIndex(index - 1);
int lastFocusLen = getScrollByFocusIndex(index);
int changeArea = lastFocusLen - indexFocusLen;
int focusMove = scroll - indexFocusLen;
float firstFocusPercent = (changeArea - focusMove) * 1f / changeArea;
float firstScale = 1 + (mCenterScale - 1) * firstFocusPercent;
mScaleArray.put(index - 1, firstScale);
float secondScale = 1 + (mCenterScale - 1) * (1 - firstFocusPercent);
mScaleArray.put(index, secondScale);
if (secondScale > firstScale) {
mFocusViewIndex = index;
mViewPickerAdapter.setNormalStyle(getChildAt(index - 1));
mViewPickerAdapter.setFocusStyle(getChildAt(index));
} else {
mFocusViewIndex = index - 1;
mViewPickerAdapter.setFocusStyle(getChildAt(index - 1));
mViewPickerAdapter.setNormalStyle(getChildAt(index));
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = mViewPickerAdapter.getCount();
if (count == 0) {
return;
}
View child = getChildAt(0);
int cL, cT, cR, cB;
int start;
int childWidth;
int childHeight;
int scaleWidth = getChildScaleWidth(child);
int scaleHeight = getChildScaleHeight(child);
if (isHorizontal()) {
start = (r - l) / 2 - scaleWidth / 2;
for (int i = 0; i < count; i++) {
child = getChildAt(i);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
scaleWidth = getChildScaleWidth(child);
cL = start + (scaleWidth - childWidth) / 2;
cT = (b - t - childHeight) / 2;
cR = cL + childWidth;
cB = cT + childHeight;
child.layout(cL, cT, cR, cB);
start += scaleWidth + mDivider;
}
} else {
start = (b - t) / 2 - scaleHeight / 2;
for (int i = 0; i < count; i++) {
child = getChildAt(i);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
scaleHeight = getChildScaleHeight(child);
cL = (r - l - childWidth) / 2;
cT = start + (scaleHeight - childHeight) / 2;
cR = cL + childWidth;
cB = cT + childHeight;
child.layout(cL, cT, cR, cB);
start += scaleHeight + mDivider;
}
}
}
private int getChildScaleWidth(View child) {
return Math.round(child.getMeasuredWidth() * child.getScaleX());
}
private int getChildScaleHeight(View child) {
return Math.round(child.getMeasuredHeight() * child.getScaleY());
}
public boolean isHorizontal() {
return mOrientation == HORIZONTAL;
}
private int getScroll() {
return isHorizontal() ? getScrollX() : getScrollY();
}
private boolean isTriggerScroll(MotionEvent event) {
if (isHorizontal()) {
return (SCALED_TOUCH_SLOP < Math.abs(event.getX() - mDownX));
}
return (SCALED_TOUCH_SLOP < Math.abs(event.getY() - mDownY));
}
private boolean snap(int move) {
int scroll = getScroll() + move;
if (scroll <= mMinScroll || scroll >= mMaxScroll) {
move(move);
return false;
}
move(move);
int len = mChildNormalSize[0] - mChildNormalSize[0] / 2;
int childCount = getChildCount();
int target;
boolean isInFocus = false;
int index = 0;
for (int i = 1; i < childCount; i++) {
index = i;
target = len + mDivider + Math.round(mChildFocusSize[i] * mCenterScale) / 2;
if (target == scroll) {
isInFocus = true;
break;
}
if (target > scroll) {
break;
}
len += mDivider + mChildNormalSize[i];
}
if (isInFocus) {
return false;
}
int desc = getScrollByFocusIndex(index - 1) - scroll;
int plus = getScrollByFocusIndex(index) - scroll;
if (Math.abs(desc) < Math.abs(plus)) {
mViewFlinger.snapSmoothScroll(desc);
} else {
mViewFlinger.snapSmoothScroll(plus);
}
return true;
}
private void overDrag() {
int scroll = getScroll();
boolean isListStart = scroll < mMinScroll;
FloatValueHolder valueHolder = new FloatValueHolder(scroll);
mOverDragRecoverAnim = new SpringAnimation(valueHolder);
SpringForce force = new SpringForce();
force.setDampingRatio(OVER_DRAGGER_DAMPING_RATIO);
force.setStiffness(OVER_DRAGGER_STIFFNESS);
mOverDragRecoverAnim.setSpring(force);
mOverDragRecoverAnim.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {
int lastValue = scroll;
@Override
public void onAnimationUpdate(DynamicAnimation animation, float value, float velocity) {
int currVal = Math.round(value);
move(currVal - lastValue);
lastValue = currVal;
}
});
mOverDragRecoverAnim.animateToFinalPosition(isListStart ? mMinScroll : mMaxScroll);
}
private void move(int move) {
int target = getScroll() + move;
if (mStatus == SCROLL_STATUS_OVER_DRAG) {
if (target < mMinScroll - mOverDragLen) {
target = mMinScroll - mOverDragLen;
}
if (target > mMaxScroll + mOverDragLen) {
target = mMaxScroll + mOverDragLen;
}
} else {
if (target < mMinScroll) {
target = mMinScroll;
}
if (target > mMaxScroll) {
target = mMaxScroll;
}
}
if (isHorizontal()) {
scrollTo(target, getScrollY());
} else {
scrollTo(getScrollX(), target);
}
requestLayout();
}
private boolean fling(int velocity) {
if (Math.abs(velocity) < mMinFlingVelocity) {
return false;
}
velocity = Math.max(-mMaxFlingVelocity, Math.min(velocity, mMaxFlingVelocity));
mViewFlinger.fling(velocity);
return true;
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (mViewPickerAdapter.isDrawFloatingUpToItems()) {
super.dispatchDraw(canvas);
drawFocusBg(canvas);
} else {
drawFocusBg(canvas);
super.dispatchDraw(canvas);
}
}
protected void drawFocusBg(Canvas canvas) {
canvas.save();
canvas.translate(getScrollX(), getScrollY());
mViewPickerAdapter.onDraw(canvas, mFocusWidth, mFocusHeight);
canvas.restore();
}
class ViewFlinger implements Runnable {
private static final int MAX_SCROLL_DURATION = 2000;
public static final int UNDEFINED_DURATION = Integer.MIN_VALUE;
private final float SPEED_PER_PIXEL;
private int mLastFling;
private final OverScroller mOverScroller;
private boolean mEatRunOnAnimationRequest = false;
private boolean mReSchedulePostAnimationCallback = false;
private ViewFlinger() {
mOverScroller = new OverScroller(getContext(), sQuinticInterpolator);
mOverScroller.setFriction(SCROLL_FRICTION);
SPEED_PER_PIXEL = MILLISECONDS_PER_INCH / getResources().getDisplayMetrics().densityDpi;
}
@Override
public void run() {
mReSchedulePostAnimationCallback = false;
mEatRunOnAnimationRequest = true;
final OverScroller scroller = mOverScroller;
boolean res = scroller.computeScrollOffset();
if (res) {
int target = isHorizontal() ? scroller.getCurrX() : scroller.getCurrY();
int unconsumed = target - mLastFling;
mLastFling = target;
boolean scrollerFinished = isHorizontal() ? scroller.getCurrX() == scroller.getFinalX() : scroller.getCurrY() == scroller.getFinalY();
final boolean doneScrolling = scroller.isFinished() || scrollerFinished;
boolean isResetScrollStatus = true;
if (unconsumed != 0) {
if (mStatus == SCROLL_STATUS_SNAP || mStatus == SCROLL_STATUS_SMOOTH) {
move(unconsumed);
}
if (mStatus == SCROLL_STATUS_INERTIA) {
if (!doneScrolling) {
move(unconsumed);
} else {
isResetScrollStatus = !snap(unconsumed);
}
}
}
if (!doneScrolling && !isNeedStopScroll()) {
postOnAnimation();
} else {
if (isResetScrollStatus) {
mStatus = SCROLL_STATUS_IDLE;
getFocusIndexByScroll();
}
}
}
mEatRunOnAnimationRequest = false;
if (mReSchedulePostAnimationCallback) {
internalPostOnAnimation();
}
}
private boolean isNeedStopScroll() {
return (mLastFling < 0 && getScroll() == 0) || (mLastFling > 0 && getScroll() == mMaxScroll);
}
void fling(int velocity) {
mStatus = SCROLL_STATUS_INERTIA;
mLastFling = 0;
if (isHorizontal()) {
mOverScroller.fling(0, 0, velocity, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
} else {
mOverScroller.fling(0, 0, 0, velocity, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
}
postOnAnimation();
}
public void snapSmoothScroll(int move) {
mStatus = SCROLL_STATUS_SNAP;
smoothScrollBy(move, calculateTimeForDeceleration(move));
}
public void smoothScrollBy(int move) {
mStatus = SCROLL_STATUS_SMOOTH;
smoothScrollBy(move, UNDEFINED_DURATION);
}
public void smoothScrollBy(int move, int duration) {
if (duration == UNDEFINED_DURATION) {
duration = computeScrollDuration(move);
}
mLastFling = 0;
if (isHorizontal()) {
mOverScroller.startScroll(0, 0, move, 0, duration);
} else {
mOverScroller.startScroll(0, 0, 0, move, duration);
}
mOverScroller.computeScrollOffset();
postOnAnimation();
}
public int calculateTimeForDeceleration(int dy) {
int timeForScrolling = (int) Math.ceil(Math.abs(dy) * SPEED_PER_PIXEL);
return (int) Math.ceil(timeForScrolling / .3356);
}
private int computeScrollDuration(int dy) {
int containerSize = getHeight();
float absDelta = Math.abs(dy);
int duration = (int) (((absDelta / containerSize) + 1) * SMOOTH_PARAM);
return Math.min(duration, MAX_SCROLL_DURATION);
}
void postOnAnimation() {
if (mEatRunOnAnimationRequest) {
mReSchedulePostAnimationCallback = true;
} else {
internalPostOnAnimation();
}
}
private void internalPostOnAnimation() {
removeCallbacks(this);
ViewCompat.postOnAnimation(ViewPicker.this, this);
}
private void cancelAnimation() {
if (!mOverScroller.isFinished()) {
mOverScroller.abortAnimation();
}
}
}
private int calculateSpringPressDistance(float distance) {
float drag = distance * OVER_DRAGGER_FRICTION;
float percent = Math.min(1f, Math.abs(drag / mOverDragLen));
return (int) (mOverDragLen * (float) ((percent / 2) - Math.pow((percent / 2), 2)) * 4f);
}
static final Interpolator sQuinticInterpolator = t -> {
t -= 1.0F;
return t * t * t * t * t + 1.0f;
};
public void setOnViewPickerListener(OnViewPickerListener listener) {
if (listener == null) {
listener = DEFAULT_LISTENER;
}
this.mOnViewPickerListener = listener;
}
}
package com.xunrui.camera.ui.commonui.viewpicker;
import android.graphics.Canvas;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import androidx.annotation.CallSuper;
public class ViewPickerAdapter<T> extends BaseAdapter {
protected boolean sIsCenterSizeSameAsNormal = true;
@Override
public int getCount() {
return 0;
}
@Override
public T getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return null;
}
protected void setFocusStyle(View view) {
}
protected void setNormalStyle(View view) {
}
@CallSuper
protected void childScale(View child, float scale) {
child.setScaleX(scale);
child.setScaleY(scale);
}
public void focusStateChanged(boolean isInFocus, View focus, View lastFocus) {
}
public boolean isCenterSizeSameAsNormal() {
return sIsCenterSizeSameAsNormal;
}
public boolean willNotDraw() {
return true;
}
public boolean isDrawFloatingUpToItems() {
return false;
}
public void onDraw(Canvas canvas, float focusWidth, float focusHeight) {
}
}
package com.xunrui.camera.ui.commonui.viewpicker;
import android.annotation.SuppressLint;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.xunrui.camera.R;
import com.xunrui.camera.app.CameraApp;
import com.xunrui.camera.ui.vo.FilterViewItem;
import java.util.List;
public class FilterPickerAdapter extends ViewPickerAdapter<FilterViewItem> {
private List<FilterViewItem> mDatas;
private final int mItemSize;
private final Drawable mCenterDrawable;
@SuppressLint("UseCompatLoadingForDrawables")
public FilterPickerAdapter(List<FilterViewItem> datas) {
this.mDatas = datas;
Resources resources = CameraApp.getContext().getResources();
mItemSize = resources.getDimensionPixelSize(R.dimen.filter_item_size);
mCenterDrawable = resources.getDrawable(R.drawable.filter_image_bg, null);
mCenterDrawable.setBounds(0, 0, mCenterDrawable.getIntrinsicWidth(), mCenterDrawable.getIntrinsicHeight());
}
public void setDatas(List<FilterViewItem> datas) {
this.mDatas = datas;
notifyDataSetChanged();
}
@Override
public int getCount() {
return mDatas.size();
}
@Override
public FilterViewItem getItem(int position) {
return mDatas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView view = new ImageView(parent.getContext());
FilterViewItem item = getItem(position);
view.setImageResource(item.getIconId());
view.setFocusable(true);
view.setScaleType(ImageView.ScaleType.FIT_CENTER);
view.setContentDescription(item.getName());
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(mItemSize, mItemSize);
view.setLayoutParams(params);
return view;
}
@Override
public void onDraw(Canvas canvas, float focusWidth, float focusHeight) {
canvas.save();
canvas.translate((canvas.getWidth() - mCenterDrawable.getIntrinsicWidth()) / 2f,
(canvas.getHeight() - mCenterDrawable.getIntrinsicHeight()) / 2f);
mCenterDrawable.draw(canvas);
canvas.restore();
}
@Override
public boolean willNotDraw() {
return false;
}
@Override
public boolean isDrawFloatingUpToItems() {
return true;
}
}
package com.yb.camx.view.viewpicker;
import android.view.View;
public interface OnViewPickerListener {
default void onSelected(int focusIndex, View view) {
}
}
<resources>
<declare-styleable name="ViewPicker">
<attr name="centerScale" format="float" />
<attr name="itemDivider" format="dimension"/>
<attr name="android:orientation" format="integer" />
</declare-styleable>
</resources>
<com.yb.camx.view.viewpicker.ViewPicker
android:id="@+id/module_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginBottom="180dp"
android:orientation="horizontal"
android:visibility="visible"
app:centerScale="1.2"
app:itemDivider="10dp" />