1497 lines
56 KiB
Java
1497 lines
56 KiB
Java
![]() |
/* Copyright (C) 2010 The Android Open Source 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 android.widget;
|
||
|
|
||
|
import android.animation.ObjectAnimator;
|
||
|
import android.animation.PropertyValuesHolder;
|
||
|
import android.content.Context;
|
||
|
import android.content.res.TypedArray;
|
||
|
import android.graphics.Bitmap;
|
||
|
import android.graphics.BlurMaskFilter;
|
||
|
import android.graphics.Canvas;
|
||
|
import android.graphics.Matrix;
|
||
|
import android.graphics.Paint;
|
||
|
import android.graphics.PorterDuff;
|
||
|
import android.graphics.PorterDuffXfermode;
|
||
|
import android.graphics.Rect;
|
||
|
import android.graphics.RectF;
|
||
|
import android.graphics.TableMaskFilter;
|
||
|
import android.os.Bundle;
|
||
|
import android.util.AttributeSet;
|
||
|
import android.util.Log;
|
||
|
import android.view.InputDevice;
|
||
|
import android.view.MotionEvent;
|
||
|
import android.view.VelocityTracker;
|
||
|
import android.view.View;
|
||
|
import android.view.ViewConfiguration;
|
||
|
import android.view.ViewGroup;
|
||
|
import android.view.accessibility.AccessibilityNodeInfo;
|
||
|
import android.view.animation.LinearInterpolator;
|
||
|
import android.widget.RemoteViews.RemoteView;
|
||
|
|
||
|
import com.android.internal.R;
|
||
|
|
||
|
import java.lang.ref.WeakReference;
|
||
|
|
||
|
@RemoteView
|
||
|
/**
|
||
|
* A view that displays its children in a stack and allows users to discretely swipe
|
||
|
* through the children.
|
||
|
*/
|
||
|
public class StackView extends AdapterViewAnimator {
|
||
|
private final String TAG = "StackView";
|
||
|
|
||
|
/**
|
||
|
* Default animation parameters
|
||
|
*/
|
||
|
private static final int DEFAULT_ANIMATION_DURATION = 400;
|
||
|
private static final int MINIMUM_ANIMATION_DURATION = 50;
|
||
|
private static final int STACK_RELAYOUT_DURATION = 100;
|
||
|
|
||
|
/**
|
||
|
* Parameters effecting the perspective visuals
|
||
|
*/
|
||
|
private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.1f;
|
||
|
private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.1f;
|
||
|
|
||
|
private float mPerspectiveShiftX;
|
||
|
private float mPerspectiveShiftY;
|
||
|
private float mNewPerspectiveShiftX;
|
||
|
private float mNewPerspectiveShiftY;
|
||
|
|
||
|
@SuppressWarnings({"FieldCanBeLocal"})
|
||
|
private static final float PERSPECTIVE_SCALE_FACTOR = 0f;
|
||
|
|
||
|
/**
|
||
|
* Represent the two possible stack modes, one where items slide up, and the other
|
||
|
* where items slide down. The perspective is also inverted between these two modes.
|
||
|
*/
|
||
|
private static final int ITEMS_SLIDE_UP = 0;
|
||
|
private static final int ITEMS_SLIDE_DOWN = 1;
|
||
|
|
||
|
/**
|
||
|
* These specify the different gesture states
|
||
|
*/
|
||
|
private static final int GESTURE_NONE = 0;
|
||
|
private static final int GESTURE_SLIDE_UP = 1;
|
||
|
private static final int GESTURE_SLIDE_DOWN = 2;
|
||
|
|
||
|
/**
|
||
|
* Specifies how far you need to swipe (up or down) before it
|
||
|
* will be consider a completed gesture when you lift your finger
|
||
|
*/
|
||
|
private static final float SWIPE_THRESHOLD_RATIO = 0.2f;
|
||
|
|
||
|
/**
|
||
|
* Specifies the total distance, relative to the size of the stack,
|
||
|
* that views will be slid, either up or down
|
||
|
*/
|
||
|
private static final float SLIDE_UP_RATIO = 0.7f;
|
||
|
|
||
|
/**
|
||
|
* Sentinel value for no current active pointer.
|
||
|
* Used by {@link #mActivePointerId}.
|
||
|
*/
|
||
|
private static final int INVALID_POINTER = -1;
|
||
|
|
||
|
/**
|
||
|
* Number of active views in the stack. One fewer view is actually visible, as one is hidden.
|
||
|
*/
|
||
|
private static final int NUM_ACTIVE_VIEWS = 5;
|
||
|
|
||
|
private static final int FRAME_PADDING = 4;
|
||
|
|
||
|
private final Rect mTouchRect = new Rect();
|
||
|
|
||
|
private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000;
|
||
|
|
||
|
private static final long MIN_TIME_BETWEEN_SCROLLS = 100;
|
||
|
|
||
|
/**
|
||
|
* These variables are all related to the current state of touch interaction
|
||
|
* with the stack
|
||
|
*/
|
||
|
private float mInitialY;
|
||
|
private float mInitialX;
|
||
|
private int mActivePointerId;
|
||
|
private int mYVelocity = 0;
|
||
|
private int mSwipeGestureType = GESTURE_NONE;
|
||
|
private int mSlideAmount;
|
||
|
private int mSwipeThreshold;
|
||
|
private int mTouchSlop;
|
||
|
private int mMaximumVelocity;
|
||
|
private VelocityTracker mVelocityTracker;
|
||
|
private boolean mTransitionIsSetup = false;
|
||
|
private int mResOutColor;
|
||
|
private int mClickColor;
|
||
|
|
||
|
private static HolographicHelper sHolographicHelper;
|
||
|
private ImageView mHighlight;
|
||
|
private ImageView mClickFeedback;
|
||
|
private boolean mClickFeedbackIsValid = false;
|
||
|
private StackSlider mStackSlider;
|
||
|
private boolean mFirstLayoutHappened = false;
|
||
|
private long mLastInteractionTime = 0;
|
||
|
private long mLastScrollTime;
|
||
|
private int mStackMode;
|
||
|
private int mFramePadding;
|
||
|
private final Rect stackInvalidateRect = new Rect();
|
||
|
|
||
|
/**
|
||
|
* {@inheritDoc}
|
||
|
*/
|
||
|
public StackView(Context context) {
|
||
|
this(context, null);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* {@inheritDoc}
|
||
|
*/
|
||
|
public StackView(Context context, AttributeSet attrs) {
|
||
|
this(context, attrs, com.android.internal.R.attr.stackViewStyle);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* {@inheritDoc}
|
||
|
*/
|
||
|
public StackView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||
|
this(context, attrs, defStyleAttr, 0);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* {@inheritDoc}
|
||
|
*/
|
||
|
public StackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||
|
final TypedArray a = context.obtainStyledAttributes(
|
||
|
attrs, com.android.internal.R.styleable.StackView, defStyleAttr, defStyleRes);
|
||
|
saveAttributeDataForStyleable(context, com.android.internal.R.styleable.StackView,
|
||
|
attrs, a, defStyleAttr, defStyleRes);
|
||
|
|
||
|
mResOutColor = a.getColor(
|
||
|
com.android.internal.R.styleable.StackView_resOutColor, 0);
|
||
|
mClickColor = a.getColor(
|
||
|
com.android.internal.R.styleable.StackView_clickColor, 0);
|
||
|
|
||
|
a.recycle();
|
||
|
initStackView();
|
||
|
}
|
||
|
|
||
|
private void initStackView() {
|
||
|
configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
|
||
|
setStaticTransformationsEnabled(true);
|
||
|
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
|
||
|
mTouchSlop = configuration.getScaledTouchSlop();
|
||
|
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
|
||
|
mActivePointerId = INVALID_POINTER;
|
||
|
|
||
|
mHighlight = new ImageView(getContext());
|
||
|
mHighlight.setLayoutParams(new LayoutParams(mHighlight));
|
||
|
addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
|
||
|
|
||
|
mClickFeedback = new ImageView(getContext());
|
||
|
mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback));
|
||
|
addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback));
|
||
|
mClickFeedback.setVisibility(INVISIBLE);
|
||
|
|
||
|
mStackSlider = new StackSlider();
|
||
|
|
||
|
if (sHolographicHelper == null) {
|
||
|
sHolographicHelper = new HolographicHelper(mContext);
|
||
|
}
|
||
|
setClipChildren(false);
|
||
|
setClipToPadding(false);
|
||
|
|
||
|
// This sets the form of the StackView, which is currently to have the perspective-shifted
|
||
|
// views above the active view, and have items slide down when sliding out. The opposite is
|
||
|
// available by using ITEMS_SLIDE_UP.
|
||
|
mStackMode = ITEMS_SLIDE_DOWN;
|
||
|
|
||
|
// This is a flag to indicate the the stack is loading for the first time
|
||
|
mWhichChild = -1;
|
||
|
|
||
|
// Adjust the frame padding based on the density, since the highlight changes based
|
||
|
// on the density
|
||
|
final float density = mContext.getResources().getDisplayMetrics().density;
|
||
|
mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Animate the views between different relative indexes within the {@link AdapterViewAnimator}
|
||
|
*/
|
||
|
void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) {
|
||
|
if (!animate) {
|
||
|
((StackFrame) view).cancelSliderAnimator();
|
||
|
view.setRotationX(0f);
|
||
|
LayoutParams lp = (LayoutParams) view.getLayoutParams();
|
||
|
lp.setVerticalOffset(0);
|
||
|
lp.setHorizontalOffset(0);
|
||
|
}
|
||
|
|
||
|
if (fromIndex == -1 && toIndex == getNumActiveViews() -1) {
|
||
|
transformViewAtIndex(toIndex, view, false);
|
||
|
view.setVisibility(VISIBLE);
|
||
|
view.setAlpha(1.0f);
|
||
|
} else if (fromIndex == 0 && toIndex == 1) {
|
||
|
// Slide item in
|
||
|
((StackFrame) view).cancelSliderAnimator();
|
||
|
view.setVisibility(VISIBLE);
|
||
|
|
||
|
int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
|
||
|
StackSlider animationSlider = new StackSlider(mStackSlider);
|
||
|
animationSlider.setView(view);
|
||
|
|
||
|
if (animate) {
|
||
|
PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
|
||
|
PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
|
||
|
ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
|
||
|
slideInX, slideInY);
|
||
|
slideIn.setDuration(duration);
|
||
|
slideIn.setInterpolator(new LinearInterpolator());
|
||
|
((StackFrame) view).setSliderAnimator(slideIn);
|
||
|
slideIn.start();
|
||
|
} else {
|
||
|
animationSlider.setYProgress(0f);
|
||
|
animationSlider.setXProgress(0f);
|
||
|
}
|
||
|
} else if (fromIndex == 1 && toIndex == 0) {
|
||
|
// Slide item out
|
||
|
((StackFrame) view).cancelSliderAnimator();
|
||
|
int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
|
||
|
|
||
|
StackSlider animationSlider = new StackSlider(mStackSlider);
|
||
|
animationSlider.setView(view);
|
||
|
if (animate) {
|
||
|
PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
|
||
|
PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
|
||
|
ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
|
||
|
slideOutX, slideOutY);
|
||
|
slideOut.setDuration(duration);
|
||
|
slideOut.setInterpolator(new LinearInterpolator());
|
||
|
((StackFrame) view).setSliderAnimator(slideOut);
|
||
|
slideOut.start();
|
||
|
} else {
|
||
|
animationSlider.setYProgress(1.0f);
|
||
|
animationSlider.setXProgress(0f);
|
||
|
}
|
||
|
} else if (toIndex == 0) {
|
||
|
// Make sure this view that is "waiting in the wings" is invisible
|
||
|
view.setAlpha(0.0f);
|
||
|
view.setVisibility(INVISIBLE);
|
||
|
} else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) {
|
||
|
view.setVisibility(VISIBLE);
|
||
|
view.setAlpha(1.0f);
|
||
|
view.setRotationX(0f);
|
||
|
LayoutParams lp = (LayoutParams) view.getLayoutParams();
|
||
|
lp.setVerticalOffset(0);
|
||
|
lp.setHorizontalOffset(0);
|
||
|
} else if (fromIndex == -1) {
|
||
|
view.setAlpha(1.0f);
|
||
|
view.setVisibility(VISIBLE);
|
||
|
} else if (toIndex == -1) {
|
||
|
if (animate) {
|
||
|
postDelayed(new Runnable() {
|
||
|
public void run() {
|
||
|
view.setAlpha(0);
|
||
|
}
|
||
|
}, STACK_RELAYOUT_DURATION);
|
||
|
} else {
|
||
|
view.setAlpha(0f);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Implement the faked perspective
|
||
|
if (toIndex != -1) {
|
||
|
transformViewAtIndex(toIndex, view, animate);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void transformViewAtIndex(int index, final View view, boolean animate) {
|
||
|
final float maxPerspectiveShiftY = mPerspectiveShiftY;
|
||
|
final float maxPerspectiveShiftX = mPerspectiveShiftX;
|
||
|
|
||
|
if (mStackMode == ITEMS_SLIDE_DOWN) {
|
||
|
index = mMaxNumActiveViews - index - 1;
|
||
|
if (index == mMaxNumActiveViews - 1) index--;
|
||
|
} else {
|
||
|
index--;
|
||
|
if (index < 0) index++;
|
||
|
}
|
||
|
|
||
|
float r = (index * 1.0f) / (mMaxNumActiveViews - 2);
|
||
|
|
||
|
final float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r);
|
||
|
|
||
|
float perspectiveTranslationY = r * maxPerspectiveShiftY;
|
||
|
float scaleShiftCorrectionY = (scale - 1) *
|
||
|
(getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f);
|
||
|
final float transY = perspectiveTranslationY + scaleShiftCorrectionY;
|
||
|
|
||
|
float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX;
|
||
|
float scaleShiftCorrectionX = (1 - scale) *
|
||
|
(getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f);
|
||
|
final float transX = perspectiveTranslationX + scaleShiftCorrectionX;
|
||
|
|
||
|
// If this view is currently being animated for a certain position, we need to cancel
|
||
|
// this animation so as not to interfere with the new transformation.
|
||
|
if (view instanceof StackFrame) {
|
||
|
((StackFrame) view).cancelTransformAnimator();
|
||
|
}
|
||
|
|
||
|
if (animate) {
|
||
|
PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX);
|
||
|
PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
|
||
|
PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale);
|
||
|
PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale);
|
||
|
|
||
|
ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY,
|
||
|
translationY, translationX);
|
||
|
oa.setDuration(STACK_RELAYOUT_DURATION);
|
||
|
if (view instanceof StackFrame) {
|
||
|
((StackFrame) view).setTransformAnimator(oa);
|
||
|
}
|
||
|
oa.start();
|
||
|
} else {
|
||
|
view.setTranslationX(transX);
|
||
|
view.setTranslationY(transY);
|
||
|
view.setScaleX(scale);
|
||
|
view.setScaleY(scale);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void setupStackSlider(View v, int mode) {
|
||
|
mStackSlider.setMode(mode);
|
||
|
if (v != null) {
|
||
|
mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor));
|
||
|
mHighlight.setRotation(v.getRotation());
|
||
|
mHighlight.setTranslationY(v.getTranslationY());
|
||
|
mHighlight.setTranslationX(v.getTranslationX());
|
||
|
mHighlight.bringToFront();
|
||
|
v.bringToFront();
|
||
|
mStackSlider.setView(v);
|
||
|
|
||
|
v.setVisibility(VISIBLE);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* {@inheritDoc}
|
||
|
*/
|
||
|
@Override
|
||
|
@android.view.RemotableViewMethod
|
||
|
public void showNext() {
|
||
|
if (mSwipeGestureType != GESTURE_NONE) return;
|
||
|
if (!mTransitionIsSetup) {
|
||
|
View v = getViewAtRelativeIndex(1);
|
||
|
if (v != null) {
|
||
|
setupStackSlider(v, StackSlider.NORMAL_MODE);
|
||
|
mStackSlider.setYProgress(0);
|
||
|
mStackSlider.setXProgress(0);
|
||
|
}
|
||
|
}
|
||
|
super.showNext();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* {@inheritDoc}
|
||
|
*/
|
||
|
@Override
|
||
|
@android.view.RemotableViewMethod
|
||
|
public void showPrevious() {
|
||
|
if (mSwipeGestureType != GESTURE_NONE) return;
|
||
|
if (!mTransitionIsSetup) {
|
||
|
View v = getViewAtRelativeIndex(0);
|
||
|
if (v != null) {
|
||
|
setupStackSlider(v, StackSlider.NORMAL_MODE);
|
||
|
mStackSlider.setYProgress(1);
|
||
|
mStackSlider.setXProgress(0);
|
||
|
}
|
||
|
}
|
||
|
super.showPrevious();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
void showOnly(int childIndex, boolean animate) {
|
||
|
super.showOnly(childIndex, animate);
|
||
|
|
||
|
// Here we need to make sure that the z-order of the children is correct
|
||
|
for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
|
||
|
int index = modulo(i, getWindowSize());
|
||
|
ViewAndMetaData vm = mViewsMap.get(index);
|
||
|
if (vm != null) {
|
||
|
View v = mViewsMap.get(index).view;
|
||
|
if (v != null) v.bringToFront();
|
||
|
}
|
||
|
}
|
||
|
if (mHighlight != null) {
|
||
|
mHighlight.bringToFront();
|
||
|
}
|
||
|
mTransitionIsSetup = false;
|
||
|
mClickFeedbackIsValid = false;
|
||
|
}
|
||
|
|
||
|
void updateClickFeedback() {
|
||
|
if (!mClickFeedbackIsValid) {
|
||
|
View v = getViewAtRelativeIndex(1);
|
||
|
if (v != null) {
|
||
|
mClickFeedback.setImageBitmap(
|
||
|
sHolographicHelper.createClickOutline(v, mClickColor));
|
||
|
mClickFeedback.setTranslationX(v.getTranslationX());
|
||
|
mClickFeedback.setTranslationY(v.getTranslationY());
|
||
|
}
|
||
|
mClickFeedbackIsValid = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
void showTapFeedback(View v) {
|
||
|
updateClickFeedback();
|
||
|
mClickFeedback.setVisibility(VISIBLE);
|
||
|
mClickFeedback.bringToFront();
|
||
|
invalidate();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
void hideTapFeedback(View v) {
|
||
|
mClickFeedback.setVisibility(INVISIBLE);
|
||
|
invalidate();
|
||
|
}
|
||
|
|
||
|
private void updateChildTransforms() {
|
||
|
for (int i = 0; i < getNumActiveViews(); i++) {
|
||
|
View v = getViewAtRelativeIndex(i);
|
||
|
if (v != null) {
|
||
|
transformViewAtIndex(i, v, false);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class StackFrame extends FrameLayout {
|
||
|
WeakReference<ObjectAnimator> transformAnimator;
|
||
|
WeakReference<ObjectAnimator> sliderAnimator;
|
||
|
|
||
|
public StackFrame(Context context) {
|
||
|
super(context);
|
||
|
}
|
||
|
|
||
|
void setTransformAnimator(ObjectAnimator oa) {
|
||
|
transformAnimator = new WeakReference<ObjectAnimator>(oa);
|
||
|
}
|
||
|
|
||
|
void setSliderAnimator(ObjectAnimator oa) {
|
||
|
sliderAnimator = new WeakReference<ObjectAnimator>(oa);
|
||
|
}
|
||
|
|
||
|
boolean cancelTransformAnimator() {
|
||
|
if (transformAnimator != null) {
|
||
|
ObjectAnimator oa = transformAnimator.get();
|
||
|
if (oa != null) {
|
||
|
oa.cancel();
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
boolean cancelSliderAnimator() {
|
||
|
if (sliderAnimator != null) {
|
||
|
ObjectAnimator oa = sliderAnimator.get();
|
||
|
if (oa != null) {
|
||
|
oa.cancel();
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
FrameLayout getFrameForChild() {
|
||
|
StackFrame fl = new StackFrame(mContext);
|
||
|
fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding);
|
||
|
return fl;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Apply any necessary tranforms for the child that is being added.
|
||
|
*/
|
||
|
void applyTransformForChildAtIndex(View child, int relativeIndex) {
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void dispatchDraw(Canvas canvas) {
|
||
|
boolean expandClipRegion = false;
|
||
|
|
||
|
canvas.getClipBounds(stackInvalidateRect);
|
||
|
final int childCount = getChildCount();
|
||
|
for (int i = 0; i < childCount; i++) {
|
||
|
final View child = getChildAt(i);
|
||
|
LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
||
|
if ((lp.horizontalOffset == 0 && lp.verticalOffset == 0) ||
|
||
|
child.getAlpha() == 0f || child.getVisibility() != VISIBLE) {
|
||
|
lp.resetInvalidateRect();
|
||
|
}
|
||
|
Rect childInvalidateRect = lp.getInvalidateRect();
|
||
|
if (!childInvalidateRect.isEmpty()) {
|
||
|
expandClipRegion = true;
|
||
|
stackInvalidateRect.union(childInvalidateRect);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// We only expand the clip bounds if necessary.
|
||
|
if (expandClipRegion) {
|
||
|
canvas.save();
|
||
|
canvas.clipRectUnion(stackInvalidateRect);
|
||
|
super.dispatchDraw(canvas);
|
||
|
canvas.restore();
|
||
|
} else {
|
||
|
super.dispatchDraw(canvas);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void onLayout() {
|
||
|
if (!mFirstLayoutHappened) {
|
||
|
mFirstLayoutHappened = true;
|
||
|
updateChildTransforms();
|
||
|
}
|
||
|
|
||
|
final int newSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
|
||
|
if (mSlideAmount != newSlideAmount) {
|
||
|
mSlideAmount = newSlideAmount;
|
||
|
mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * newSlideAmount);
|
||
|
}
|
||
|
|
||
|
if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 ||
|
||
|
Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) {
|
||
|
|
||
|
mPerspectiveShiftY = mNewPerspectiveShiftY;
|
||
|
mPerspectiveShiftX = mNewPerspectiveShiftX;
|
||
|
updateChildTransforms();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
||
|
if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
|
||
|
switch (event.getAction()) {
|
||
|
case MotionEvent.ACTION_SCROLL: {
|
||
|
final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||
|
if (vscroll < 0) {
|
||
|
pacedScroll(false);
|
||
|
return true;
|
||
|
} else if (vscroll > 0) {
|
||
|
pacedScroll(true);
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return super.onGenericMotionEvent(event);
|
||
|
}
|
||
|
|
||
|
// This ensures that the frequency of stack flips caused by scrolls is capped
|
||
|
private void pacedScroll(boolean up) {
|
||
|
long timeSinceLastScroll = System.currentTimeMillis() - mLastScrollTime;
|
||
|
if (timeSinceLastScroll > MIN_TIME_BETWEEN_SCROLLS) {
|
||
|
if (up) {
|
||
|
showPrevious();
|
||
|
} else {
|
||
|
showNext();
|
||
|
}
|
||
|
mLastScrollTime = System.currentTimeMillis();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* {@inheritDoc}
|
||
|
*/
|
||
|
@Override
|
||
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||
|
int action = ev.getAction();
|
||
|
switch(action & MotionEvent.ACTION_MASK) {
|
||
|
case MotionEvent.ACTION_DOWN: {
|
||
|
if (mActivePointerId == INVALID_POINTER) {
|
||
|
mInitialX = ev.getX();
|
||
|
mInitialY = ev.getY();
|
||
|
mActivePointerId = ev.getPointerId(0);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case MotionEvent.ACTION_MOVE: {
|
||
|
int pointerIndex = ev.findPointerIndex(mActivePointerId);
|
||
|
if (pointerIndex == INVALID_POINTER) {
|
||
|
// no data for our primary pointer, this shouldn't happen, log it
|
||
|
Log.d(TAG, "Error: No data for our primary pointer.");
|
||
|
return false;
|
||
|
}
|
||
|
float newY = ev.getY(pointerIndex);
|
||
|
float deltaY = newY - mInitialY;
|
||
|
|
||
|
beginGestureIfNeeded(deltaY);
|
||
|
break;
|
||
|
}
|
||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||
|
onSecondaryPointerUp(ev);
|
||
|
break;
|
||
|
}
|
||
|
case MotionEvent.ACTION_UP:
|
||
|
case MotionEvent.ACTION_CANCEL: {
|
||
|
mActivePointerId = INVALID_POINTER;
|
||
|
mSwipeGestureType = GESTURE_NONE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return mSwipeGestureType != GESTURE_NONE;
|
||
|
}
|
||
|
|
||
|
private void beginGestureIfNeeded(float deltaY) {
|
||
|
if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
|
||
|
final int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
|
||
|
cancelLongPress();
|
||
|
requestDisallowInterceptTouchEvent(true);
|
||
|
|
||
|
if (mAdapter == null) return;
|
||
|
final int adapterCount = getCount();
|
||
|
|
||
|
int activeIndex;
|
||
|
if (mStackMode == ITEMS_SLIDE_UP) {
|
||
|
activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
|
||
|
} else {
|
||
|
activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0;
|
||
|
}
|
||
|
|
||
|
boolean endOfStack = mLoopViews && adapterCount == 1
|
||
|
&& ((mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_UP)
|
||
|
|| (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_DOWN));
|
||
|
boolean beginningOfStack = mLoopViews && adapterCount == 1
|
||
|
&& ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_UP)
|
||
|
|| (mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_DOWN));
|
||
|
|
||
|
int stackMode;
|
||
|
if (mLoopViews && !beginningOfStack && !endOfStack) {
|
||
|
stackMode = StackSlider.NORMAL_MODE;
|
||
|
} else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) {
|
||
|
activeIndex++;
|
||
|
stackMode = StackSlider.BEGINNING_OF_STACK_MODE;
|
||
|
} else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) {
|
||
|
stackMode = StackSlider.END_OF_STACK_MODE;
|
||
|
} else {
|
||
|
stackMode = StackSlider.NORMAL_MODE;
|
||
|
}
|
||
|
|
||
|
mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE;
|
||
|
|
||
|
View v = getViewAtRelativeIndex(activeIndex);
|
||
|
if (v == null) return;
|
||
|
|
||
|
setupStackSlider(v, stackMode);
|
||
|
|
||
|
// We only register this gesture if we've made it this far without a problem
|
||
|
mSwipeGestureType = swipeGestureType;
|
||
|
cancelHandleClick();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* {@inheritDoc}
|
||
|
*/
|
||
|
@Override
|
||
|
public boolean onTouchEvent(MotionEvent ev) {
|
||
|
super.onTouchEvent(ev);
|
||
|
|
||
|
int action = ev.getAction();
|
||
|
int pointerIndex = ev.findPointerIndex(mActivePointerId);
|
||
|
if (pointerIndex == INVALID_POINTER) {
|
||
|
// no data for our primary pointer, this shouldn't happen, log it
|
||
|
Log.d(TAG, "Error: No data for our primary pointer.");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
float newY = ev.getY(pointerIndex);
|
||
|
float newX = ev.getX(pointerIndex);
|
||
|
float deltaY = newY - mInitialY;
|
||
|
float deltaX = newX - mInitialX;
|
||
|
if (mVelocityTracker == null) {
|
||
|
mVelocityTracker = VelocityTracker.obtain();
|
||
|
}
|
||
|
mVelocityTracker.addMovement(ev);
|
||
|
|
||
|
switch (action & MotionEvent.ACTION_MASK) {
|
||
|
case MotionEvent.ACTION_MOVE: {
|
||
|
beginGestureIfNeeded(deltaY);
|
||
|
|
||
|
float rx = deltaX / (mSlideAmount * 1.0f);
|
||
|
if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
|
||
|
float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
|
||
|
if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
|
||
|
mStackSlider.setYProgress(1 - r);
|
||
|
mStackSlider.setXProgress(rx);
|
||
|
return true;
|
||
|
} else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
|
||
|
float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
|
||
|
if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
|
||
|
mStackSlider.setYProgress(r);
|
||
|
mStackSlider.setXProgress(rx);
|
||
|
return true;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case MotionEvent.ACTION_UP: {
|
||
|
handlePointerUp(ev);
|
||
|
break;
|
||
|
}
|
||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||
|
onSecondaryPointerUp(ev);
|
||
|
break;
|
||
|
}
|
||
|
case MotionEvent.ACTION_CANCEL: {
|
||
|
mActivePointerId = INVALID_POINTER;
|
||
|
mSwipeGestureType = GESTURE_NONE;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private void onSecondaryPointerUp(MotionEvent ev) {
|
||
|
final int activePointerIndex = ev.getActionIndex();
|
||
|
final int pointerId = ev.getPointerId(activePointerIndex);
|
||
|
if (pointerId == mActivePointerId) {
|
||
|
|
||
|
int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
|
||
|
|
||
|
View v = getViewAtRelativeIndex(activeViewIndex);
|
||
|
if (v == null) return;
|
||
|
|
||
|
// Our primary pointer has gone up -- let's see if we can find
|
||
|
// another pointer on the view. If so, then we should replace
|
||
|
// our primary pointer with this new pointer and adjust things
|
||
|
// so that the view doesn't jump
|
||
|
for (int index = 0; index < ev.getPointerCount(); index++) {
|
||
|
if (index != activePointerIndex) {
|
||
|
|
||
|
float x = ev.getX(index);
|
||
|
float y = ev.getY(index);
|
||
|
|
||
|
mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
|
||
|
if (mTouchRect.contains(Math.round(x), Math.round(y))) {
|
||
|
float oldX = ev.getX(activePointerIndex);
|
||
|
float oldY = ev.getY(activePointerIndex);
|
||
|
|
||
|
// adjust our frame of reference to avoid a jump
|
||
|
mInitialY += (y - oldY);
|
||
|
mInitialX += (x - oldX);
|
||
|
|
||
|
mActivePointerId = ev.getPointerId(index);
|
||
|
if (mVelocityTracker != null) {
|
||
|
mVelocityTracker.clear();
|
||
|
}
|
||
|
// ok, we're good, we found a new pointer which is touching the active view
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// if we made it this far, it means we didn't find a satisfactory new pointer :(,
|
||
|
// so end the gesture
|
||
|
handlePointerUp(ev);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void handlePointerUp(MotionEvent ev) {
|
||
|
int pointerIndex = ev.findPointerIndex(mActivePointerId);
|
||
|
float newY = ev.getY(pointerIndex);
|
||
|
int deltaY = (int) (newY - mInitialY);
|
||
|
mLastInteractionTime = System.currentTimeMillis();
|
||
|
|
||
|
if (mVelocityTracker != null) {
|
||
|
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
|
||
|
mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
|
||
|
}
|
||
|
|
||
|
if (mVelocityTracker != null) {
|
||
|
mVelocityTracker.recycle();
|
||
|
mVelocityTracker = null;
|
||
|
}
|
||
|
|
||
|
if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
|
||
|
&& mStackSlider.mMode == StackSlider.NORMAL_MODE) {
|
||
|
// We reset the gesture variable, because otherwise we will ignore showPrevious() /
|
||
|
// showNext();
|
||
|
mSwipeGestureType = GESTURE_NONE;
|
||
|
|
||
|
// Swipe threshold exceeded, swipe down
|
||
|
if (mStackMode == ITEMS_SLIDE_UP) {
|
||
|
showPrevious();
|
||
|
} else {
|
||
|
showNext();
|
||
|
}
|
||
|
mHighlight.bringToFront();
|
||
|
} else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
|
||
|
&& mStackSlider.mMode == StackSlider.NORMAL_MODE) {
|
||
|
// We reset the gesture variable, because otherwise we will ignore showPrevious() /
|
||
|
// showNext();
|
||
|
mSwipeGestureType = GESTURE_NONE;
|
||
|
|
||
|
// Swipe threshold exceeded, swipe up
|
||
|
if (mStackMode == ITEMS_SLIDE_UP) {
|
||
|
showNext();
|
||
|
} else {
|
||
|
showPrevious();
|
||
|
}
|
||
|
|
||
|
mHighlight.bringToFront();
|
||
|
} else if (mSwipeGestureType == GESTURE_SLIDE_UP ) {
|
||
|
// Didn't swipe up far enough, snap back down
|
||
|
int duration;
|
||
|
float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
|
||
|
if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
|
||
|
duration = Math.round(mStackSlider.getDurationForNeutralPosition());
|
||
|
} else {
|
||
|
duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
|
||
|
}
|
||
|
|
||
|
StackSlider animationSlider = new StackSlider(mStackSlider);
|
||
|
PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
|
||
|
PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
|
||
|
ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
|
||
|
snapBackX, snapBackY);
|
||
|
pa.setDuration(duration);
|
||
|
pa.setInterpolator(new LinearInterpolator());
|
||
|
pa.start();
|
||
|
} else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
|
||
|
// Didn't swipe down far enough, snap back up
|
||
|
float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
|
||
|
int duration;
|
||
|
if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
|
||
|
duration = Math.round(mStackSlider.getDurationForNeutralPosition());
|
||
|
} else {
|
||
|
duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
|
||
|
}
|
||
|
|
||
|
StackSlider animationSlider = new StackSlider(mStackSlider);
|
||
|
PropertyValuesHolder snapBackY =
|
||
|
PropertyValuesHolder.ofFloat("YProgress",finalYProgress);
|
||
|
PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
|
||
|
ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
|
||
|
snapBackX, snapBackY);
|
||
|
pa.setDuration(duration);
|
||
|
pa.start();
|
||
|
}
|
||
|
|
||
|
mActivePointerId = INVALID_POINTER;
|
||
|
mSwipeGestureType = GESTURE_NONE;
|
||
|
}
|
||
|
|
||
|
private class StackSlider {
|
||
|
View mView;
|
||
|
float mYProgress;
|
||
|
float mXProgress;
|
||
|
|
||
|
static final int NORMAL_MODE = 0;
|
||
|
static final int BEGINNING_OF_STACK_MODE = 1;
|
||
|
static final int END_OF_STACK_MODE = 2;
|
||
|
|
||
|
int mMode = NORMAL_MODE;
|
||
|
|
||
|
public StackSlider() {
|
||
|
}
|
||
|
|
||
|
public StackSlider(StackSlider copy) {
|
||
|
mView = copy.mView;
|
||
|
mYProgress = copy.mYProgress;
|
||
|
mXProgress = copy.mXProgress;
|
||
|
mMode = copy.mMode;
|
||
|
}
|
||
|
|
||
|
private float cubic(float r) {
|
||
|
return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
|
||
|
}
|
||
|
|
||
|
private float highlightAlphaInterpolator(float r) {
|
||
|
float pivot = 0.4f;
|
||
|
if (r < pivot) {
|
||
|
return 0.85f * cubic(r / pivot);
|
||
|
} else {
|
||
|
return 0.85f * cubic(1 - (r - pivot) / (1 - pivot));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private float viewAlphaInterpolator(float r) {
|
||
|
float pivot = 0.3f;
|
||
|
if (r > pivot) {
|
||
|
return (r - pivot) / (1 - pivot);
|
||
|
} else {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private float rotationInterpolator(float r) {
|
||
|
float pivot = 0.2f;
|
||
|
if (r < pivot) {
|
||
|
return 0;
|
||
|
} else {
|
||
|
return (r - pivot) / (1 - pivot);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void setView(View v) {
|
||
|
mView = v;
|
||
|
}
|
||
|
|
||
|
public void setYProgress(float r) {
|
||
|
// enforce r between 0 and 1
|
||
|
r = Math.min(1.0f, r);
|
||
|
r = Math.max(0, r);
|
||
|
|
||
|
mYProgress = r;
|
||
|
if (mView == null) return;
|
||
|
|
||
|
final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
|
||
|
final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
|
||
|
|
||
|
int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
|
||
|
|
||
|
// We need to prevent any clipping issues which may arise by setting a layer type.
|
||
|
// This doesn't come for free however, so we only want to enable it when required.
|
||
|
if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) {
|
||
|
if (mView.getLayerType() == LAYER_TYPE_NONE) {
|
||
|
mView.setLayerType(LAYER_TYPE_HARDWARE, null);
|
||
|
}
|
||
|
} else {
|
||
|
if (mView.getLayerType() != LAYER_TYPE_NONE) {
|
||
|
mView.setLayerType(LAYER_TYPE_NONE, null);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch (mMode) {
|
||
|
case NORMAL_MODE:
|
||
|
viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
|
||
|
highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
|
||
|
mHighlight.setAlpha(highlightAlphaInterpolator(r));
|
||
|
|
||
|
float alpha = viewAlphaInterpolator(1 - r);
|
||
|
|
||
|
// We make sure that views which can't be seen (have 0 alpha) are also invisible
|
||
|
// so that they don't interfere with click events.
|
||
|
if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
|
||
|
mView.setVisibility(VISIBLE);
|
||
|
} else if (alpha == 0 && mView.getAlpha() != 0
|
||
|
&& mView.getVisibility() == VISIBLE) {
|
||
|
mView.setVisibility(INVISIBLE);
|
||
|
}
|
||
|
|
||
|
mView.setAlpha(alpha);
|
||
|
mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
|
||
|
mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
|
||
|
break;
|
||
|
case END_OF_STACK_MODE:
|
||
|
r = r * 0.2f;
|
||
|
viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
|
||
|
highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
|
||
|
mHighlight.setAlpha(highlightAlphaInterpolator(r));
|
||
|
break;
|
||
|
case BEGINNING_OF_STACK_MODE:
|
||
|
r = (1-r) * 0.2f;
|
||
|
viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
|
||
|
highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
|
||
|
mHighlight.setAlpha(highlightAlphaInterpolator(r));
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void setXProgress(float r) {
|
||
|
// enforce r between 0 and 1
|
||
|
r = Math.min(2.0f, r);
|
||
|
r = Math.max(-2.0f, r);
|
||
|
|
||
|
mXProgress = r;
|
||
|
|
||
|
if (mView == null) return;
|
||
|
final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
|
||
|
final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
|
||
|
|
||
|
r *= 0.2f;
|
||
|
viewLp.setHorizontalOffset(Math.round(r * mSlideAmount));
|
||
|
highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount));
|
||
|
}
|
||
|
|
||
|
void setMode(int mode) {
|
||
|
mMode = mode;
|
||
|
}
|
||
|
|
||
|
float getDurationForNeutralPosition() {
|
||
|
return getDuration(false, 0);
|
||
|
}
|
||
|
|
||
|
float getDurationForOffscreenPosition() {
|
||
|
return getDuration(true, 0);
|
||
|
}
|
||
|
|
||
|
float getDurationForNeutralPosition(float velocity) {
|
||
|
return getDuration(false, velocity);
|
||
|
}
|
||
|
|
||
|
float getDurationForOffscreenPosition(float velocity) {
|
||
|
return getDuration(true, velocity);
|
||
|
}
|
||
|
|
||
|
private float getDuration(boolean invert, float velocity) {
|
||
|
if (mView != null) {
|
||
|
final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
|
||
|
|
||
|
float d = (float) Math.hypot(viewLp.horizontalOffset, viewLp.verticalOffset);
|
||
|
float maxd = (float) Math.hypot(mSlideAmount, 0.4f * mSlideAmount);
|
||
|
if (d > maxd) {
|
||
|
// Because mSlideAmount is updated in onLayout(), it is possible that d > maxd
|
||
|
// if we get onLayout() right before this method is called.
|
||
|
d = maxd;
|
||
|
}
|
||
|
|
||
|
if (velocity == 0) {
|
||
|
return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
|
||
|
} else {
|
||
|
float duration = invert ? d / Math.abs(velocity) :
|
||
|
(maxd - d) / Math.abs(velocity);
|
||
|
if (duration < MINIMUM_ANIMATION_DURATION ||
|
||
|
duration > DEFAULT_ANIMATION_DURATION) {
|
||
|
return getDuration(invert, 0);
|
||
|
} else {
|
||
|
return duration;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// Used for animations
|
||
|
@SuppressWarnings({"UnusedDeclaration"})
|
||
|
public float getYProgress() {
|
||
|
return mYProgress;
|
||
|
}
|
||
|
|
||
|
// Used for animations
|
||
|
@SuppressWarnings({"UnusedDeclaration"})
|
||
|
public float getXProgress() {
|
||
|
return mXProgress;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
LayoutParams createOrReuseLayoutParams(View v) {
|
||
|
final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
|
||
|
if (currentLp instanceof LayoutParams) {
|
||
|
LayoutParams lp = (LayoutParams) currentLp;
|
||
|
lp.setHorizontalOffset(0);
|
||
|
lp.setVerticalOffset(0);
|
||
|
lp.width = 0;
|
||
|
lp.width = 0;
|
||
|
return lp;
|
||
|
}
|
||
|
return new LayoutParams(v);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||
|
checkForAndHandleDataChanged();
|
||
|
|
||
|
final int childCount = getChildCount();
|
||
|
for (int i = 0; i < childCount; i++) {
|
||
|
final View child = getChildAt(i);
|
||
|
|
||
|
int childRight = mPaddingLeft + child.getMeasuredWidth();
|
||
|
int childBottom = mPaddingTop + child.getMeasuredHeight();
|
||
|
LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
||
|
|
||
|
child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
|
||
|
childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
|
||
|
|
||
|
}
|
||
|
onLayout();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void advance() {
|
||
|
long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime;
|
||
|
|
||
|
if (mAdapter == null) return;
|
||
|
final int adapterCount = getCount();
|
||
|
if (adapterCount == 1 && mLoopViews) return;
|
||
|
|
||
|
if (mSwipeGestureType == GESTURE_NONE &&
|
||
|
timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) {
|
||
|
showNext();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void measureChildren() {
|
||
|
final int count = getChildCount();
|
||
|
|
||
|
final int measuredWidth = getMeasuredWidth();
|
||
|
final int measuredHeight = getMeasuredHeight();
|
||
|
|
||
|
final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X))
|
||
|
- mPaddingLeft - mPaddingRight;
|
||
|
final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y))
|
||
|
- mPaddingTop - mPaddingBottom;
|
||
|
|
||
|
int maxWidth = 0;
|
||
|
int maxHeight = 0;
|
||
|
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
final View child = getChildAt(i);
|
||
|
child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST),
|
||
|
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
|
||
|
|
||
|
if (child != mHighlight && child != mClickFeedback) {
|
||
|
final int childMeasuredWidth = child.getMeasuredWidth();
|
||
|
final int childMeasuredHeight = child.getMeasuredHeight();
|
||
|
if (childMeasuredWidth > maxWidth) {
|
||
|
maxWidth = childMeasuredWidth;
|
||
|
}
|
||
|
if (childMeasuredHeight > maxHeight) {
|
||
|
maxHeight = childMeasuredHeight;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth;
|
||
|
mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight;
|
||
|
|
||
|
// If we have extra space, we try and spread the items out
|
||
|
if (maxWidth > 0 && count > 0 && maxWidth < childWidth) {
|
||
|
mNewPerspectiveShiftX = measuredWidth - maxWidth;
|
||
|
}
|
||
|
|
||
|
if (maxHeight > 0 && count > 0 && maxHeight < childHeight) {
|
||
|
mNewPerspectiveShiftY = measuredHeight - maxHeight;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||
|
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
|
||
|
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
|
||
|
final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
|
||
|
final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
|
||
|
|
||
|
boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
|
||
|
|
||
|
// We need to deal with the case where our parent hasn't told us how
|
||
|
// big we should be. In this case we should
|
||
|
float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y);
|
||
|
if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
|
||
|
heightSpecSize = haveChildRefSize ?
|
||
|
Math.round(mReferenceChildHeight * (1 + factorY)) +
|
||
|
mPaddingTop + mPaddingBottom : 0;
|
||
|
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
|
||
|
if (haveChildRefSize) {
|
||
|
int height = Math.round(mReferenceChildHeight * (1 + factorY))
|
||
|
+ mPaddingTop + mPaddingBottom;
|
||
|
if (height <= heightSpecSize) {
|
||
|
heightSpecSize = height;
|
||
|
} else {
|
||
|
heightSpecSize |= MEASURED_STATE_TOO_SMALL;
|
||
|
|
||
|
}
|
||
|
} else {
|
||
|
heightSpecSize = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X);
|
||
|
if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
|
||
|
widthSpecSize = haveChildRefSize ?
|
||
|
Math.round(mReferenceChildWidth * (1 + factorX)) +
|
||
|
mPaddingLeft + mPaddingRight : 0;
|
||
|
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
|
||
|
if (haveChildRefSize) {
|
||
|
int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight;
|
||
|
if (width <= widthSpecSize) {
|
||
|
widthSpecSize = width;
|
||
|
} else {
|
||
|
widthSpecSize |= MEASURED_STATE_TOO_SMALL;
|
||
|
}
|
||
|
} else {
|
||
|
widthSpecSize = 0;
|
||
|
}
|
||
|
}
|
||
|
setMeasuredDimension(widthSpecSize, heightSpecSize);
|
||
|
measureChildren();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public CharSequence getAccessibilityClassName() {
|
||
|
return StackView.class.getName();
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
|
||
|
super.onInitializeAccessibilityNodeInfoInternal(info);
|
||
|
info.setScrollable(getChildCount() > 1);
|
||
|
if (isEnabled()) {
|
||
|
if (getDisplayedChild() < getChildCount() - 1) {
|
||
|
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
|
||
|
if (mStackMode == ITEMS_SLIDE_UP) {
|
||
|
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_DOWN);
|
||
|
} else {
|
||
|
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_UP);
|
||
|
}
|
||
|
}
|
||
|
if (getDisplayedChild() > 0) {
|
||
|
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
|
||
|
if (mStackMode == ITEMS_SLIDE_UP) {
|
||
|
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_UP);
|
||
|
} else {
|
||
|
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_DOWN);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private boolean goForward() {
|
||
|
if (getDisplayedChild() < getChildCount() - 1) {
|
||
|
showNext();
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
private boolean goBackward() {
|
||
|
if (getDisplayedChild() > 0) {
|
||
|
showPrevious();
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
|
||
|
if (super.performAccessibilityActionInternal(action, arguments)) {
|
||
|
return true;
|
||
|
}
|
||
|
if (!isEnabled()) {
|
||
|
return false;
|
||
|
}
|
||
|
switch (action) {
|
||
|
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
|
||
|
return goForward();
|
||
|
}
|
||
|
case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
|
||
|
return goBackward();
|
||
|
}
|
||
|
case R.id.accessibilityActionPageUp: {
|
||
|
if (mStackMode == ITEMS_SLIDE_UP) {
|
||
|
return goBackward();
|
||
|
} else {
|
||
|
return goForward();
|
||
|
}
|
||
|
}
|
||
|
case R.id.accessibilityActionPageDown: {
|
||
|
if (mStackMode == ITEMS_SLIDE_UP) {
|
||
|
return goForward();
|
||
|
} else {
|
||
|
return goBackward();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
class LayoutParams extends ViewGroup.LayoutParams {
|
||
|
int horizontalOffset;
|
||
|
int verticalOffset;
|
||
|
View mView;
|
||
|
private final Rect parentRect = new Rect();
|
||
|
private final Rect invalidateRect = new Rect();
|
||
|
private final RectF invalidateRectf = new RectF();
|
||
|
private final Rect globalInvalidateRect = new Rect();
|
||
|
|
||
|
LayoutParams(View view) {
|
||
|
super(0, 0);
|
||
|
width = 0;
|
||
|
height = 0;
|
||
|
horizontalOffset = 0;
|
||
|
verticalOffset = 0;
|
||
|
mView = view;
|
||
|
}
|
||
|
|
||
|
LayoutParams(Context c, AttributeSet attrs) {
|
||
|
super(c, attrs);
|
||
|
horizontalOffset = 0;
|
||
|
verticalOffset = 0;
|
||
|
width = 0;
|
||
|
height = 0;
|
||
|
}
|
||
|
|
||
|
void invalidateGlobalRegion(View v, Rect r) {
|
||
|
// We need to make a new rect here, so as not to modify the one passed
|
||
|
globalInvalidateRect.set(r);
|
||
|
globalInvalidateRect.union(0, 0, getWidth(), getHeight());
|
||
|
View p = v;
|
||
|
if (!(v.getParent() != null && v.getParent() instanceof View)) return;
|
||
|
|
||
|
boolean firstPass = true;
|
||
|
parentRect.set(0, 0, 0, 0);
|
||
|
while (p.getParent() != null && p.getParent() instanceof View
|
||
|
&& !parentRect.contains(globalInvalidateRect)) {
|
||
|
if (!firstPass) {
|
||
|
globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
|
||
|
- p.getScrollY());
|
||
|
}
|
||
|
firstPass = false;
|
||
|
p = (View) p.getParent();
|
||
|
parentRect.set(p.getScrollX(), p.getScrollY(),
|
||
|
p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
|
||
|
p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
|
||
|
globalInvalidateRect.right, globalInvalidateRect.bottom);
|
||
|
}
|
||
|
|
||
|
p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
|
||
|
globalInvalidateRect.right, globalInvalidateRect.bottom);
|
||
|
}
|
||
|
|
||
|
Rect getInvalidateRect() {
|
||
|
return invalidateRect;
|
||
|
}
|
||
|
|
||
|
void resetInvalidateRect() {
|
||
|
invalidateRect.set(0, 0, 0, 0);
|
||
|
}
|
||
|
|
||
|
// This is public so that ObjectAnimator can access it
|
||
|
public void setVerticalOffset(int newVerticalOffset) {
|
||
|
setOffsets(horizontalOffset, newVerticalOffset);
|
||
|
}
|
||
|
|
||
|
public void setHorizontalOffset(int newHorizontalOffset) {
|
||
|
setOffsets(newHorizontalOffset, verticalOffset);
|
||
|
}
|
||
|
|
||
|
public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
|
||
|
int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
|
||
|
horizontalOffset = newHorizontalOffset;
|
||
|
int verticalOffsetDelta = newVerticalOffset - verticalOffset;
|
||
|
verticalOffset = newVerticalOffset;
|
||
|
|
||
|
if (mView != null) {
|
||
|
mView.requestLayout();
|
||
|
int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
|
||
|
int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
|
||
|
int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
|
||
|
int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
|
||
|
|
||
|
invalidateRectf.set(left, top, right, bottom);
|
||
|
|
||
|
float xoffset = -invalidateRectf.left;
|
||
|
float yoffset = -invalidateRectf.top;
|
||
|
invalidateRectf.offset(xoffset, yoffset);
|
||
|
mView.getMatrix().mapRect(invalidateRectf);
|
||
|
invalidateRectf.offset(-xoffset, -yoffset);
|
||
|
|
||
|
invalidateRect.set((int) Math.floor(invalidateRectf.left),
|
||
|
(int) Math.floor(invalidateRectf.top),
|
||
|
(int) Math.ceil(invalidateRectf.right),
|
||
|
(int) Math.ceil(invalidateRectf.bottom));
|
||
|
|
||
|
invalidateGlobalRegion(mView, invalidateRect);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class HolographicHelper {
|
||
|
private final Paint mHolographicPaint = new Paint();
|
||
|
private final Paint mErasePaint = new Paint();
|
||
|
private final Paint mBlurPaint = new Paint();
|
||
|
private static final int RES_OUT = 0;
|
||
|
private static final int CLICK_FEEDBACK = 1;
|
||
|
private float mDensity;
|
||
|
private BlurMaskFilter mSmallBlurMaskFilter;
|
||
|
private BlurMaskFilter mLargeBlurMaskFilter;
|
||
|
private final Canvas mCanvas = new Canvas();
|
||
|
private final Canvas mMaskCanvas = new Canvas();
|
||
|
private final int[] mTmpXY = new int[2];
|
||
|
private final Matrix mIdentityMatrix = new Matrix();
|
||
|
|
||
|
HolographicHelper(Context context) {
|
||
|
mDensity = context.getResources().getDisplayMetrics().density;
|
||
|
|
||
|
mHolographicPaint.setFilterBitmap(true);
|
||
|
mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
|
||
|
mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
|
||
|
mErasePaint.setFilterBitmap(true);
|
||
|
|
||
|
mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
|
||
|
mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
|
||
|
}
|
||
|
|
||
|
Bitmap createClickOutline(View v, int color) {
|
||
|
return createOutline(v, CLICK_FEEDBACK, color);
|
||
|
}
|
||
|
|
||
|
Bitmap createResOutline(View v, int color) {
|
||
|
return createOutline(v, RES_OUT, color);
|
||
|
}
|
||
|
|
||
|
Bitmap createOutline(View v, int type, int color) {
|
||
|
mHolographicPaint.setColor(color);
|
||
|
if (type == RES_OUT) {
|
||
|
mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
|
||
|
} else if (type == CLICK_FEEDBACK) {
|
||
|
mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
|
||
|
}
|
||
|
|
||
|
if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
Bitmap bitmap = Bitmap.createBitmap(v.getResources().getDisplayMetrics(),
|
||
|
v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
|
||
|
mCanvas.setBitmap(bitmap);
|
||
|
|
||
|
float rotationX = v.getRotationX();
|
||
|
float rotation = v.getRotation();
|
||
|
float translationY = v.getTranslationY();
|
||
|
float translationX = v.getTranslationX();
|
||
|
v.setRotationX(0);
|
||
|
v.setRotation(0);
|
||
|
v.setTranslationY(0);
|
||
|
v.setTranslationX(0);
|
||
|
v.draw(mCanvas);
|
||
|
v.setRotationX(rotationX);
|
||
|
v.setRotation(rotation);
|
||
|
v.setTranslationY(translationY);
|
||
|
v.setTranslationX(translationX);
|
||
|
|
||
|
drawOutline(mCanvas, bitmap);
|
||
|
mCanvas.setBitmap(null);
|
||
|
return bitmap;
|
||
|
}
|
||
|
|
||
|
void drawOutline(Canvas dest, Bitmap src) {
|
||
|
final int[] xy = mTmpXY;
|
||
|
Bitmap mask = src.extractAlpha(mBlurPaint, xy);
|
||
|
mMaskCanvas.setBitmap(mask);
|
||
|
mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
|
||
|
dest.drawColor(0, PorterDuff.Mode.CLEAR);
|
||
|
dest.setMatrix(mIdentityMatrix);
|
||
|
dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
|
||
|
mMaskCanvas.setBitmap(null);
|
||
|
mask.recycle();
|
||
|
}
|
||
|
}
|
||
|
}
|