Focus normally proceeds top down through the view tree. Change the order of focus so that the security fields have a chance to receive focus before the widget fields. Fixes bug 11203748. Change-Id: Ie7f16e59652edbe64fa8342197fda81b53976360
1291 lines
48 KiB
Java
1291 lines
48 KiB
Java
/*
|
|
* Copyright (C) 2012 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 com.android.keyguard;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.ObjectAnimator;
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.content.res.TypedArray;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Rect;
|
|
import android.util.AttributeSet;
|
|
import android.util.DisplayMetrics;
|
|
import android.util.FloatProperty;
|
|
import android.util.Log;
|
|
import android.util.Property;
|
|
import android.view.MotionEvent;
|
|
import android.view.VelocityTracker;
|
|
import android.view.View;
|
|
import android.view.ViewConfiguration;
|
|
import android.view.ViewGroup;
|
|
import android.view.accessibility.AccessibilityManager;
|
|
import android.view.animation.Interpolator;
|
|
import android.widget.Scroller;
|
|
|
|
/**
|
|
* This layout handles interaction with the sliding security challenge views
|
|
* that overlay/resize other keyguard contents.
|
|
*/
|
|
public class SlidingChallengeLayout extends ViewGroup implements ChallengeLayout {
|
|
private static final String TAG = "SlidingChallengeLayout";
|
|
private static final boolean DEBUG = false;
|
|
|
|
// The drag handle is measured in dp above & below the top edge of the
|
|
// challenge view; these parameters change based on whether the challenge
|
|
// is open or closed.
|
|
private static final int DRAG_HANDLE_CLOSED_ABOVE = 8; // dp
|
|
private static final int DRAG_HANDLE_CLOSED_BELOW = 0; // dp
|
|
private static final int DRAG_HANDLE_OPEN_ABOVE = 8; // dp
|
|
private static final int DRAG_HANDLE_OPEN_BELOW = 0; // dp
|
|
|
|
private static final int HANDLE_ANIMATE_DURATION = 250; // ms
|
|
|
|
// Drawn to show the drag handle in closed state; crossfades to the challenge view
|
|
// when challenge is fully visible
|
|
private boolean mEdgeCaptured;
|
|
|
|
private DisplayMetrics mDisplayMetrics;
|
|
|
|
// Initialized during measurement from child layoutparams
|
|
private View mExpandChallengeView;
|
|
private KeyguardSecurityContainer mChallengeView;
|
|
private View mScrimView;
|
|
private View mWidgetsView;
|
|
|
|
// Range: 0 (fully hidden) to 1 (fully visible)
|
|
private float mChallengeOffset = 1.f;
|
|
private boolean mChallengeShowing = true;
|
|
private boolean mChallengeShowingTargetState = true;
|
|
private boolean mWasChallengeShowing = true;
|
|
private boolean mIsBouncing = false;
|
|
|
|
private final Scroller mScroller;
|
|
private ObjectAnimator mFader;
|
|
private int mScrollState;
|
|
private OnChallengeScrolledListener mScrollListener;
|
|
private OnBouncerStateChangedListener mBouncerListener;
|
|
private boolean mEnableChallengeDragging;
|
|
|
|
public static final int SCROLL_STATE_IDLE = 0;
|
|
public static final int SCROLL_STATE_DRAGGING = 1;
|
|
public static final int SCROLL_STATE_SETTLING = 2;
|
|
public static final int SCROLL_STATE_FADING = 3;
|
|
|
|
public static final int CHALLENGE_FADE_OUT_DURATION = 100;
|
|
public static final int CHALLENGE_FADE_IN_DURATION = 160;
|
|
|
|
private static final int MAX_SETTLE_DURATION = 600; // ms
|
|
|
|
// ID of the pointer in charge of a current drag
|
|
private int mActivePointerId = INVALID_POINTER;
|
|
private static final int INVALID_POINTER = -1;
|
|
|
|
// True if the user is currently dragging the slider
|
|
private boolean mDragging;
|
|
// True if the user may not drag until a new gesture begins
|
|
private boolean mBlockDrag;
|
|
|
|
private VelocityTracker mVelocityTracker;
|
|
private int mMinVelocity;
|
|
private int mMaxVelocity;
|
|
private float mGestureStartX, mGestureStartY; // where did you first touch the screen?
|
|
private int mGestureStartChallengeBottom; // where was the challenge at that time?
|
|
|
|
private int mDragHandleClosedBelow; // handle hitrect extension into the challenge view
|
|
private int mDragHandleClosedAbove; // extend the handle's hitrect this far above the line
|
|
private int mDragHandleOpenBelow; // handle hitrect extension into the challenge view
|
|
private int mDragHandleOpenAbove; // extend the handle's hitrect this far above the line
|
|
|
|
private int mDragHandleEdgeSlop;
|
|
private int mChallengeBottomBound; // Number of pixels from the top of the challenge view
|
|
// that should remain on-screen
|
|
|
|
private int mTouchSlop;
|
|
private int mTouchSlopSquare;
|
|
|
|
float mHandleAlpha;
|
|
float mFrameAlpha;
|
|
float mFrameAnimationTarget = Float.MIN_VALUE;
|
|
private ObjectAnimator mHandleAnimation;
|
|
private ObjectAnimator mFrameAnimation;
|
|
|
|
private boolean mHasGlowpad;
|
|
private final Rect mInsets = new Rect();
|
|
|
|
// We have an internal and external version, and we and them together.
|
|
private boolean mChallengeInteractiveExternal = true;
|
|
private boolean mChallengeInteractiveInternal = true;
|
|
|
|
static final Property<SlidingChallengeLayout, Float> HANDLE_ALPHA =
|
|
new FloatProperty<SlidingChallengeLayout>("handleAlpha") {
|
|
@Override
|
|
public void setValue(SlidingChallengeLayout view, float value) {
|
|
view.mHandleAlpha = value;
|
|
view.invalidate();
|
|
}
|
|
|
|
@Override
|
|
public Float get(SlidingChallengeLayout view) {
|
|
return view.mHandleAlpha;
|
|
}
|
|
};
|
|
|
|
// True if at least one layout pass has happened since the view was attached.
|
|
private boolean mHasLayout;
|
|
|
|
private static final Interpolator sMotionInterpolator = new Interpolator() {
|
|
public float getInterpolation(float t) {
|
|
t -= 1.0f;
|
|
return t * t * t * t * t + 1.0f;
|
|
}
|
|
};
|
|
|
|
private static final Interpolator sHandleFadeInterpolator = new Interpolator() {
|
|
public float getInterpolation(float t) {
|
|
return t * t;
|
|
}
|
|
};
|
|
|
|
private final Runnable mEndScrollRunnable = new Runnable () {
|
|
public void run() {
|
|
completeChallengeScroll();
|
|
}
|
|
};
|
|
|
|
private final OnClickListener mScrimClickListener = new OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
hideBouncer();
|
|
}
|
|
};
|
|
|
|
private final OnClickListener mExpandChallengeClickListener = new OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
if (!isChallengeShowing()) {
|
|
showChallenge(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Listener interface that reports changes in scroll state of the challenge area.
|
|
*/
|
|
public interface OnChallengeScrolledListener {
|
|
/**
|
|
* The scroll state itself changed.
|
|
*
|
|
* <p>scrollState will be one of the following:</p>
|
|
*
|
|
* <ul>
|
|
* <li><code>SCROLL_STATE_IDLE</code> - The challenge area is stationary.</li>
|
|
* <li><code>SCROLL_STATE_DRAGGING</code> - The user is actively dragging
|
|
* the challenge area.</li>
|
|
* <li><code>SCROLL_STATE_SETTLING</code> - The challenge area is animating
|
|
* into place.</li>
|
|
* </ul>
|
|
*
|
|
* <p>Do not perform expensive operations (e.g. layout)
|
|
* while the scroll state is not <code>SCROLL_STATE_IDLE</code>.</p>
|
|
*
|
|
* @param scrollState The new scroll state of the challenge area.
|
|
*/
|
|
public void onScrollStateChanged(int scrollState);
|
|
|
|
/**
|
|
* The precise position of the challenge area has changed.
|
|
*
|
|
* <p>NOTE: It is NOT safe to modify layout or call any View methods that may
|
|
* result in a requestLayout anywhere in your view hierarchy as a result of this call.
|
|
* It may be called during drawing.</p>
|
|
*
|
|
* @param scrollPosition New relative position of the challenge area.
|
|
* 1.f = fully visible/ready to be interacted with.
|
|
* 0.f = fully invisible/inaccessible to the user.
|
|
* @param challengeTop Position of the top edge of the challenge view in px in the
|
|
* SlidingChallengeLayout's coordinate system.
|
|
*/
|
|
public void onScrollPositionChanged(float scrollPosition, int challengeTop);
|
|
}
|
|
|
|
public SlidingChallengeLayout(Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public SlidingChallengeLayout(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public SlidingChallengeLayout(Context context, AttributeSet attrs, int defStyle) {
|
|
super(context, attrs, defStyle);
|
|
|
|
mScroller = new Scroller(context, sMotionInterpolator);
|
|
|
|
final ViewConfiguration vc = ViewConfiguration.get(context);
|
|
mMinVelocity = vc.getScaledMinimumFlingVelocity();
|
|
mMaxVelocity = vc.getScaledMaximumFlingVelocity();
|
|
|
|
final Resources res = getResources();
|
|
mDragHandleEdgeSlop = res.getDimensionPixelSize(R.dimen.kg_edge_swipe_region_size);
|
|
|
|
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
|
mTouchSlopSquare = mTouchSlop * mTouchSlop;
|
|
|
|
mDisplayMetrics = res.getDisplayMetrics();
|
|
final float density = mDisplayMetrics.density;
|
|
|
|
// top half of the lock icon, plus another 25% to be sure
|
|
mDragHandleClosedAbove = (int) (DRAG_HANDLE_CLOSED_ABOVE * density + 0.5f);
|
|
mDragHandleClosedBelow = (int) (DRAG_HANDLE_CLOSED_BELOW * density + 0.5f);
|
|
mDragHandleOpenAbove = (int) (DRAG_HANDLE_OPEN_ABOVE * density + 0.5f);
|
|
mDragHandleOpenBelow = (int) (DRAG_HANDLE_OPEN_BELOW * density + 0.5f);
|
|
|
|
// how much space to account for in the handle when closed
|
|
mChallengeBottomBound = res.getDimensionPixelSize(R.dimen.kg_widget_pager_bottom_padding);
|
|
|
|
setWillNotDraw(false);
|
|
setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
|
|
}
|
|
|
|
public void setEnableChallengeDragging(boolean enabled) {
|
|
mEnableChallengeDragging = enabled;
|
|
}
|
|
|
|
public void setInsets(Rect insets) {
|
|
mInsets.set(insets);
|
|
}
|
|
|
|
public void setHandleAlpha(float alpha) {
|
|
if (mExpandChallengeView != null) {
|
|
mExpandChallengeView.setAlpha(alpha);
|
|
}
|
|
}
|
|
|
|
public void setChallengeInteractive(boolean interactive) {
|
|
mChallengeInteractiveExternal = interactive;
|
|
if (mExpandChallengeView != null) {
|
|
mExpandChallengeView.setEnabled(interactive);
|
|
}
|
|
}
|
|
|
|
void animateHandle(boolean visible) {
|
|
if (mHandleAnimation != null) {
|
|
mHandleAnimation.cancel();
|
|
mHandleAnimation = null;
|
|
}
|
|
final float targetAlpha = visible ? 1.f : 0.f;
|
|
if (targetAlpha == mHandleAlpha) {
|
|
return;
|
|
}
|
|
mHandleAnimation = ObjectAnimator.ofFloat(this, HANDLE_ALPHA, targetAlpha);
|
|
mHandleAnimation.setInterpolator(sHandleFadeInterpolator);
|
|
mHandleAnimation.setDuration(HANDLE_ANIMATE_DURATION);
|
|
mHandleAnimation.start();
|
|
}
|
|
|
|
private void sendInitialListenerUpdates() {
|
|
if (mScrollListener != null) {
|
|
int challengeTop = mChallengeView != null ? mChallengeView.getTop() : 0;
|
|
mScrollListener.onScrollPositionChanged(mChallengeOffset, challengeTop);
|
|
mScrollListener.onScrollStateChanged(mScrollState);
|
|
}
|
|
}
|
|
|
|
public void setOnChallengeScrolledListener(OnChallengeScrolledListener listener) {
|
|
mScrollListener = listener;
|
|
if (mHasLayout) {
|
|
sendInitialListenerUpdates();
|
|
}
|
|
}
|
|
|
|
public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) {
|
|
mBouncerListener = listener;
|
|
}
|
|
|
|
@Override
|
|
public void onAttachedToWindow() {
|
|
super.onAttachedToWindow();
|
|
|
|
mHasLayout = false;
|
|
}
|
|
|
|
@Override
|
|
public void onDetachedFromWindow() {
|
|
super.onDetachedFromWindow();
|
|
|
|
removeCallbacks(mEndScrollRunnable);
|
|
mHasLayout = false;
|
|
}
|
|
|
|
@Override
|
|
public void requestChildFocus(View child, View focused) {
|
|
if (mIsBouncing && child != mChallengeView) {
|
|
// Clear out of the bouncer if the user tries to move focus outside of
|
|
// the security challenge view.
|
|
hideBouncer();
|
|
}
|
|
super.requestChildFocus(child, focused);
|
|
}
|
|
|
|
// We want the duration of the page snap animation to be influenced by the distance that
|
|
// the screen has to travel, however, we don't want this duration to be effected in a
|
|
// purely linear fashion. Instead, we use this method to moderate the effect that the distance
|
|
// of travel has on the overall snap duration.
|
|
float distanceInfluenceForSnapDuration(float f) {
|
|
f -= 0.5f; // center the values about 0.
|
|
f *= 0.3f * Math.PI / 2.0f;
|
|
return (float) Math.sin(f);
|
|
}
|
|
|
|
void setScrollState(int state) {
|
|
if (mScrollState != state) {
|
|
mScrollState = state;
|
|
|
|
animateHandle(state == SCROLL_STATE_IDLE && !mChallengeShowing);
|
|
if (mScrollListener != null) {
|
|
mScrollListener.onScrollStateChanged(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
void completeChallengeScroll() {
|
|
setChallengeShowing(mChallengeShowingTargetState);
|
|
mChallengeOffset = mChallengeShowing ? 1.f : 0.f;
|
|
setScrollState(SCROLL_STATE_IDLE);
|
|
mChallengeInteractiveInternal = true;
|
|
mChallengeView.setLayerType(LAYER_TYPE_NONE, null);
|
|
}
|
|
|
|
void setScrimView(View scrim) {
|
|
if (mScrimView != null) {
|
|
mScrimView.setOnClickListener(null);
|
|
}
|
|
mScrimView = scrim;
|
|
if (mScrimView != null) {
|
|
mScrimView.setVisibility(mIsBouncing ? VISIBLE : GONE);
|
|
mScrimView.setFocusable(true);
|
|
mScrimView.setOnClickListener(mScrimClickListener);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Animate the bottom edge of the challenge view to the given position.
|
|
*
|
|
* @param y desired final position for the bottom edge of the challenge view in px
|
|
* @param velocity velocity in
|
|
*/
|
|
void animateChallengeTo(int y, int velocity) {
|
|
if (mChallengeView == null) {
|
|
// Nothing to do.
|
|
return;
|
|
}
|
|
|
|
cancelTransitionsInProgress();
|
|
|
|
mChallengeInteractiveInternal = false;
|
|
enableHardwareLayerForChallengeView();
|
|
final int sy = mChallengeView.getBottom();
|
|
final int dy = y - sy;
|
|
if (dy == 0) {
|
|
completeChallengeScroll();
|
|
return;
|
|
}
|
|
|
|
setScrollState(SCROLL_STATE_SETTLING);
|
|
|
|
final int childHeight = mChallengeView.getHeight();
|
|
final int halfHeight = childHeight / 2;
|
|
final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / childHeight);
|
|
final float distance = halfHeight + halfHeight *
|
|
distanceInfluenceForSnapDuration(distanceRatio);
|
|
|
|
int duration = 0;
|
|
velocity = Math.abs(velocity);
|
|
if (velocity > 0) {
|
|
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
|
|
} else {
|
|
final float childDelta = (float) Math.abs(dy) / childHeight;
|
|
duration = (int) ((childDelta + 1) * 100);
|
|
}
|
|
duration = Math.min(duration, MAX_SETTLE_DURATION);
|
|
|
|
mScroller.startScroll(0, sy, 0, dy, duration);
|
|
postInvalidateOnAnimation();
|
|
}
|
|
|
|
private void setChallengeShowing(boolean showChallenge) {
|
|
if (mChallengeShowing == showChallenge) {
|
|
return;
|
|
}
|
|
mChallengeShowing = showChallenge;
|
|
|
|
if (mExpandChallengeView == null || mChallengeView == null) {
|
|
// These might not be here yet if we haven't been through layout.
|
|
// If we haven't, the first layout pass will set everything up correctly
|
|
// based on mChallengeShowing as set above.
|
|
return;
|
|
}
|
|
|
|
if (mChallengeShowing) {
|
|
mExpandChallengeView.setVisibility(View.INVISIBLE);
|
|
mChallengeView.setVisibility(View.VISIBLE);
|
|
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
|
|
mChallengeView.requestAccessibilityFocus();
|
|
mChallengeView.announceForAccessibility(mContext.getString(
|
|
R.string.keyguard_accessibility_unlock_area_expanded));
|
|
}
|
|
} else {
|
|
mExpandChallengeView.setVisibility(View.VISIBLE);
|
|
mChallengeView.setVisibility(View.INVISIBLE);
|
|
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
|
|
mExpandChallengeView.requestAccessibilityFocus();
|
|
mChallengeView.announceForAccessibility(mContext.getString(
|
|
R.string.keyguard_accessibility_unlock_area_collapsed));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return true if the challenge is at all visible.
|
|
*/
|
|
public boolean isChallengeShowing() {
|
|
return mChallengeShowing;
|
|
}
|
|
|
|
@Override
|
|
public boolean isChallengeOverlapping() {
|
|
return mChallengeShowing;
|
|
}
|
|
|
|
@Override
|
|
public boolean isBouncing() {
|
|
return mIsBouncing;
|
|
}
|
|
|
|
@Override
|
|
public int getBouncerAnimationDuration() {
|
|
return HANDLE_ANIMATE_DURATION;
|
|
}
|
|
|
|
@Override
|
|
public void showBouncer() {
|
|
if (mIsBouncing) return;
|
|
setSystemUiVisibility(getSystemUiVisibility() | STATUS_BAR_DISABLE_SEARCH);
|
|
mWasChallengeShowing = mChallengeShowing;
|
|
mIsBouncing = true;
|
|
showChallenge(true);
|
|
if (mScrimView != null) {
|
|
Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 1f);
|
|
anim.setDuration(HANDLE_ANIMATE_DURATION);
|
|
anim.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationStart(Animator animation) {
|
|
mScrimView.setVisibility(VISIBLE);
|
|
}
|
|
});
|
|
anim.start();
|
|
}
|
|
if (mChallengeView != null) {
|
|
mChallengeView.showBouncer(HANDLE_ANIMATE_DURATION);
|
|
}
|
|
|
|
if (mBouncerListener != null) {
|
|
mBouncerListener.onBouncerStateChanged(true);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void hideBouncer() {
|
|
if (!mIsBouncing) return;
|
|
setSystemUiVisibility(getSystemUiVisibility() & ~STATUS_BAR_DISABLE_SEARCH);
|
|
if (!mWasChallengeShowing) showChallenge(false);
|
|
mIsBouncing = false;
|
|
|
|
if (mScrimView != null) {
|
|
Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 0f);
|
|
anim.setDuration(HANDLE_ANIMATE_DURATION);
|
|
anim.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
mScrimView.setVisibility(GONE);
|
|
}
|
|
});
|
|
anim.start();
|
|
}
|
|
if (mChallengeView != null) {
|
|
mChallengeView.hideBouncer(HANDLE_ANIMATE_DURATION);
|
|
}
|
|
if (mBouncerListener != null) {
|
|
mBouncerListener.onBouncerStateChanged(false);
|
|
}
|
|
}
|
|
|
|
private int getChallengeMargin(boolean expanded) {
|
|
return expanded && mHasGlowpad ? 0 : mDragHandleEdgeSlop;
|
|
}
|
|
|
|
private float getChallengeAlpha() {
|
|
float x = mChallengeOffset - 1;
|
|
return x * x * x + 1.f;
|
|
}
|
|
|
|
@Override
|
|
public void requestDisallowInterceptTouchEvent(boolean allowIntercept) {
|
|
// We'll intercept whoever we feel like! ...as long as it isn't a challenge view.
|
|
// If there are one or more pointers in the challenge view before we take over
|
|
// touch events, onInterceptTouchEvent will set mBlockDrag.
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
|
if (mVelocityTracker == null) {
|
|
mVelocityTracker = VelocityTracker.obtain();
|
|
}
|
|
mVelocityTracker.addMovement(ev);
|
|
|
|
final int action = ev.getActionMasked();
|
|
switch (action) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
mGestureStartX = ev.getX();
|
|
mGestureStartY = ev.getY();
|
|
mBlockDrag = false;
|
|
break;
|
|
|
|
case MotionEvent.ACTION_CANCEL:
|
|
case MotionEvent.ACTION_UP:
|
|
resetTouch();
|
|
break;
|
|
|
|
case MotionEvent.ACTION_MOVE:
|
|
final int count = ev.getPointerCount();
|
|
for (int i = 0; i < count; i++) {
|
|
final float x = ev.getX(i);
|
|
final float y = ev.getY(i);
|
|
if (!mIsBouncing && mActivePointerId == INVALID_POINTER
|
|
&& (crossedDragHandle(x, y, mGestureStartY)
|
|
&& shouldEnableChallengeDragging()
|
|
|| (isInChallengeView(x, y) &&
|
|
mScrollState == SCROLL_STATE_SETTLING))) {
|
|
mActivePointerId = ev.getPointerId(i);
|
|
mGestureStartX = x;
|
|
mGestureStartY = y;
|
|
mGestureStartChallengeBottom = getChallengeBottom();
|
|
mDragging = true;
|
|
enableHardwareLayerForChallengeView();
|
|
} else if (mChallengeShowing && isInChallengeView(x, y)
|
|
&& shouldEnableChallengeDragging()) {
|
|
mBlockDrag = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (mBlockDrag || isChallengeInteractionBlocked()) {
|
|
mActivePointerId = INVALID_POINTER;
|
|
mDragging = false;
|
|
}
|
|
|
|
return mDragging;
|
|
}
|
|
|
|
private boolean shouldEnableChallengeDragging() {
|
|
return mEnableChallengeDragging || !mChallengeShowing;
|
|
}
|
|
|
|
private boolean isChallengeInteractionBlocked() {
|
|
return !mChallengeInteractiveExternal || !mChallengeInteractiveInternal;
|
|
}
|
|
|
|
private void resetTouch() {
|
|
mVelocityTracker.recycle();
|
|
mVelocityTracker = null;
|
|
mActivePointerId = INVALID_POINTER;
|
|
mDragging = mBlockDrag = false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent ev) {
|
|
if (mVelocityTracker == null) {
|
|
mVelocityTracker = VelocityTracker.obtain();
|
|
}
|
|
mVelocityTracker.addMovement(ev);
|
|
|
|
final int action = ev.getActionMasked();
|
|
switch (action) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
mBlockDrag = false;
|
|
mGestureStartX = ev.getX();
|
|
mGestureStartY = ev.getY();
|
|
break;
|
|
|
|
case MotionEvent.ACTION_CANCEL:
|
|
if (mDragging && !isChallengeInteractionBlocked()) {
|
|
showChallenge(0);
|
|
}
|
|
resetTouch();
|
|
break;
|
|
|
|
case MotionEvent.ACTION_POINTER_UP:
|
|
if (mActivePointerId != ev.getPointerId(ev.getActionIndex())) {
|
|
break;
|
|
}
|
|
case MotionEvent.ACTION_UP:
|
|
if (mDragging && !isChallengeInteractionBlocked()) {
|
|
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
|
|
showChallenge((int) mVelocityTracker.getYVelocity(mActivePointerId));
|
|
}
|
|
resetTouch();
|
|
break;
|
|
|
|
case MotionEvent.ACTION_MOVE:
|
|
if (!mDragging && !mBlockDrag && !mIsBouncing) {
|
|
final int count = ev.getPointerCount();
|
|
for (int i = 0; i < count; i++) {
|
|
final float x = ev.getX(i);
|
|
final float y = ev.getY(i);
|
|
|
|
if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mGestureStartY) ||
|
|
(isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING))
|
|
&& mActivePointerId == INVALID_POINTER
|
|
&& !isChallengeInteractionBlocked()) {
|
|
mGestureStartX = x;
|
|
mGestureStartY = y;
|
|
mActivePointerId = ev.getPointerId(i);
|
|
mGestureStartChallengeBottom = getChallengeBottom();
|
|
mDragging = true;
|
|
enableHardwareLayerForChallengeView();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Not an else; this can be set above.
|
|
if (mDragging) {
|
|
// No-op if already in this state, but set it here in case we arrived
|
|
// at this point from either intercept or the above.
|
|
setScrollState(SCROLL_STATE_DRAGGING);
|
|
|
|
final int index = ev.findPointerIndex(mActivePointerId);
|
|
if (index < 0) {
|
|
// Oops, bogus state. We lost some touch events somewhere.
|
|
// Just drop it with no velocity and let things settle.
|
|
resetTouch();
|
|
showChallenge(0);
|
|
return true;
|
|
}
|
|
final float y = ev.getY(index);
|
|
final float pos = Math.min(y - mGestureStartY,
|
|
getLayoutBottom() - mChallengeBottomBound);
|
|
|
|
moveChallengeTo(mGestureStartChallengeBottom + (int) pos);
|
|
}
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* The lifecycle of touch events is subtle and it's very easy to do something
|
|
* that will cause bugs that will be nasty to track when overriding this method.
|
|
* Normally one should always override onInterceptTouchEvent instead.
|
|
*
|
|
* To put it another way, don't try this at home.
|
|
*/
|
|
@Override
|
|
public boolean dispatchTouchEvent(MotionEvent ev) {
|
|
final int action = ev.getActionMasked();
|
|
boolean handled = false;
|
|
if (action == MotionEvent.ACTION_DOWN) {
|
|
// Defensive programming: if we didn't get the UP or CANCEL, reset anyway.
|
|
mEdgeCaptured = false;
|
|
}
|
|
if (mWidgetsView != null && !mIsBouncing && (mEdgeCaptured || isEdgeSwipeBeginEvent(ev))) {
|
|
// Normally we would need to do a lot of extra stuff here.
|
|
// We can only get away with this because we haven't padded in
|
|
// the widget pager or otherwise transformed it during layout.
|
|
// We also don't support things like splitting MotionEvents.
|
|
|
|
// We set handled to captured even if dispatch is returning false here so that
|
|
// we don't send a different view a busted or incomplete event stream.
|
|
handled = mEdgeCaptured |= mWidgetsView.dispatchTouchEvent(ev);
|
|
}
|
|
|
|
if (!handled && !mEdgeCaptured) {
|
|
handled = super.dispatchTouchEvent(ev);
|
|
}
|
|
|
|
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
mEdgeCaptured = false;
|
|
}
|
|
|
|
return handled;
|
|
}
|
|
|
|
private boolean isEdgeSwipeBeginEvent(MotionEvent ev) {
|
|
if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
|
|
return false;
|
|
}
|
|
|
|
final float x = ev.getX();
|
|
return x < mDragHandleEdgeSlop || x >= getWidth() - mDragHandleEdgeSlop;
|
|
}
|
|
|
|
/**
|
|
* We only want to add additional vertical space to the drag handle when the panel is fully
|
|
* closed.
|
|
*/
|
|
private int getDragHandleSizeAbove() {
|
|
return isChallengeShowing() ? mDragHandleOpenAbove : mDragHandleClosedAbove;
|
|
}
|
|
private int getDragHandleSizeBelow() {
|
|
return isChallengeShowing() ? mDragHandleOpenBelow : mDragHandleClosedBelow;
|
|
}
|
|
|
|
private boolean isInChallengeView(float x, float y) {
|
|
return isPointInView(x, y, mChallengeView);
|
|
}
|
|
|
|
private boolean isInDragHandle(float x, float y) {
|
|
return isPointInView(x, y, mExpandChallengeView);
|
|
}
|
|
|
|
private boolean isPointInView(float x, float y, View view) {
|
|
if (view == null) {
|
|
return false;
|
|
}
|
|
return x >= view.getLeft() && y >= view.getTop()
|
|
&& x < view.getRight() && y < view.getBottom();
|
|
}
|
|
|
|
private boolean crossedDragHandle(float x, float y, float initialY) {
|
|
|
|
final int challengeTop = mChallengeView.getTop();
|
|
final boolean horizOk = x >= 0 && x < getWidth();
|
|
|
|
final boolean vertOk;
|
|
if (mChallengeShowing) {
|
|
vertOk = initialY < (challengeTop - getDragHandleSizeAbove()) &&
|
|
y > challengeTop + getDragHandleSizeBelow();
|
|
} else {
|
|
vertOk = initialY > challengeTop + getDragHandleSizeBelow() &&
|
|
y < challengeTop - getDragHandleSizeAbove();
|
|
}
|
|
return horizOk && vertOk;
|
|
}
|
|
|
|
private int makeChildMeasureSpec(int maxSize, int childDimen) {
|
|
final int mode;
|
|
final int size;
|
|
switch (childDimen) {
|
|
case LayoutParams.WRAP_CONTENT:
|
|
mode = MeasureSpec.AT_MOST;
|
|
size = maxSize;
|
|
break;
|
|
case LayoutParams.MATCH_PARENT:
|
|
mode = MeasureSpec.EXACTLY;
|
|
size = maxSize;
|
|
break;
|
|
default:
|
|
mode = MeasureSpec.EXACTLY;
|
|
size = Math.min(maxSize, childDimen);
|
|
break;
|
|
}
|
|
return MeasureSpec.makeMeasureSpec(size, mode);
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(int widthSpec, int heightSpec) {
|
|
if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY ||
|
|
MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) {
|
|
throw new IllegalArgumentException(
|
|
"SlidingChallengeLayout must be measured with an exact size");
|
|
}
|
|
final int width = MeasureSpec.getSize(widthSpec);
|
|
final int height = MeasureSpec.getSize(heightSpec);
|
|
setMeasuredDimension(width, height);
|
|
|
|
final int insetHeight = height - mInsets.top - mInsets.bottom;
|
|
final int insetHeightSpec = MeasureSpec.makeMeasureSpec(insetHeight, MeasureSpec.EXACTLY);
|
|
|
|
// Find one and only one challenge view.
|
|
final View oldChallengeView = mChallengeView;
|
|
final View oldExpandChallengeView = mChallengeView;
|
|
mChallengeView = null;
|
|
mExpandChallengeView = null;
|
|
final int count = getChildCount();
|
|
|
|
// First iteration through the children finds special children and sets any associated
|
|
// state.
|
|
for (int i = 0; i < count; i++) {
|
|
final View child = getChildAt(i);
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) {
|
|
if (mChallengeView != null) {
|
|
throw new IllegalStateException(
|
|
"There may only be one child with layout_isChallenge=\"true\"");
|
|
}
|
|
if (!(child instanceof KeyguardSecurityContainer)) {
|
|
throw new IllegalArgumentException(
|
|
"Challenge must be a KeyguardSecurityContainer");
|
|
}
|
|
mChallengeView = (KeyguardSecurityContainer) child;
|
|
if (mChallengeView != oldChallengeView) {
|
|
mChallengeView.setVisibility(mChallengeShowing ? VISIBLE : INVISIBLE);
|
|
}
|
|
// We're going to play silly games with the frame's background drawable later.
|
|
if (!mHasLayout) {
|
|
// Set up the margin correctly based on our content for the first run.
|
|
mHasGlowpad = child.findViewById(R.id.keyguard_selector_view) != null;
|
|
lp.leftMargin = lp.rightMargin = getChallengeMargin(true);
|
|
}
|
|
} else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) {
|
|
if (mExpandChallengeView != null) {
|
|
throw new IllegalStateException(
|
|
"There may only be one child with layout_childType"
|
|
+ "=\"expandChallengeHandle\"");
|
|
}
|
|
mExpandChallengeView = child;
|
|
if (mExpandChallengeView != oldExpandChallengeView) {
|
|
mExpandChallengeView.setVisibility(mChallengeShowing ? INVISIBLE : VISIBLE);
|
|
mExpandChallengeView.setOnClickListener(mExpandChallengeClickListener);
|
|
}
|
|
} else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) {
|
|
setScrimView(child);
|
|
} else if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) {
|
|
mWidgetsView = child;
|
|
}
|
|
}
|
|
|
|
// We want to measure the challenge view first, since the KeyguardWidgetPager
|
|
// needs to do things its measure pass that are dependent on the challenge view
|
|
// having been measured.
|
|
if (mChallengeView != null && mChallengeView.getVisibility() != View.GONE) {
|
|
// This one's a little funny. If the IME is present - reported in the form
|
|
// of insets on the root view - we only give the challenge the space it would
|
|
// have had if the IME wasn't there in order to keep the rest of the layout stable.
|
|
// We base this on the layout_maxHeight on the challenge view. If it comes out
|
|
// negative or zero, either we didn't have a maxHeight or we're totally out of space,
|
|
// so give up and measure as if this rule weren't there.
|
|
int challengeHeightSpec = insetHeightSpec;
|
|
final View root = getRootView();
|
|
if (root != null) {
|
|
final LayoutParams lp = (LayoutParams) mChallengeView.getLayoutParams();
|
|
final int windowHeight = mDisplayMetrics.heightPixels
|
|
- root.getPaddingTop() - mInsets.top;
|
|
final int diff = windowHeight - insetHeight;
|
|
final int maxChallengeHeight = lp.maxHeight - diff;
|
|
if (maxChallengeHeight > 0) {
|
|
challengeHeightSpec = makeChildMeasureSpec(maxChallengeHeight, lp.height);
|
|
}
|
|
}
|
|
measureChildWithMargins(mChallengeView, widthSpec, 0, challengeHeightSpec, 0);
|
|
}
|
|
|
|
// Measure the rest of the children
|
|
for (int i = 0; i < count; i++) {
|
|
final View child = getChildAt(i);
|
|
if (child.getVisibility() == GONE) {
|
|
continue;
|
|
}
|
|
// Don't measure the challenge view twice!
|
|
if (child == mChallengeView) continue;
|
|
|
|
// Measure children. Widget frame measures special, so that we can ignore
|
|
// insets for the IME.
|
|
int parentWidthSpec = widthSpec, parentHeightSpec = insetHeightSpec;
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) {
|
|
final View root = getRootView();
|
|
if (root != null) {
|
|
// This calculation is super dodgy and relies on several assumptions.
|
|
// Specifically that the root of the window will be padded in for insets
|
|
// and that the window is LAYOUT_IN_SCREEN.
|
|
final int windowWidth = mDisplayMetrics.widthPixels;
|
|
final int windowHeight = mDisplayMetrics.heightPixels
|
|
- root.getPaddingTop() - mInsets.top;
|
|
parentWidthSpec = MeasureSpec.makeMeasureSpec(
|
|
windowWidth, MeasureSpec.EXACTLY);
|
|
parentHeightSpec = MeasureSpec.makeMeasureSpec(
|
|
windowHeight, MeasureSpec.EXACTLY);
|
|
}
|
|
} else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) {
|
|
// Allow scrim views to extend into the insets
|
|
parentWidthSpec = widthSpec;
|
|
parentHeightSpec = heightSpec;
|
|
}
|
|
measureChildWithMargins(child, parentWidthSpec, 0, parentHeightSpec, 0);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
final int paddingLeft = getPaddingLeft();
|
|
final int paddingTop = getPaddingTop();
|
|
final int paddingRight = getPaddingRight();
|
|
final int paddingBottom = getPaddingBottom();
|
|
final int width = r - l;
|
|
final int height = b - t;
|
|
|
|
final int count = getChildCount();
|
|
for (int i = 0; i < count; i++) {
|
|
final View child = getChildAt(i);
|
|
|
|
if (child.getVisibility() == GONE) continue;
|
|
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
|
|
if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) {
|
|
// Challenge views pin to the bottom, offset by a portion of their height,
|
|
// and center horizontally.
|
|
final int center = (paddingLeft + width - paddingRight) / 2;
|
|
final int childWidth = child.getMeasuredWidth();
|
|
final int childHeight = child.getMeasuredHeight();
|
|
final int left = center - childWidth / 2;
|
|
final int layoutBottom = height - paddingBottom - lp.bottomMargin - mInsets.bottom;
|
|
// We use the top of the challenge view to position the handle, so
|
|
// we never want less than the handle size showing at the bottom.
|
|
final int bottom = layoutBottom + (int) ((childHeight - mChallengeBottomBound)
|
|
* (1 - mChallengeOffset));
|
|
child.setAlpha(getChallengeAlpha());
|
|
child.layout(left, bottom - childHeight, left + childWidth, bottom);
|
|
} else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) {
|
|
final int center = (paddingLeft + width - paddingRight) / 2;
|
|
final int left = center - child.getMeasuredWidth() / 2;
|
|
final int right = left + child.getMeasuredWidth();
|
|
final int bottom = height - paddingBottom - lp.bottomMargin - mInsets.bottom;
|
|
final int top = bottom - child.getMeasuredHeight();
|
|
child.layout(left, top, right, bottom);
|
|
} else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) {
|
|
// Scrim views use the entire area, including padding & insets
|
|
child.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
|
|
} else {
|
|
// Non-challenge views lay out from the upper left, layered.
|
|
child.layout(paddingLeft + lp.leftMargin,
|
|
paddingTop + lp.topMargin + mInsets.top,
|
|
paddingLeft + child.getMeasuredWidth(),
|
|
paddingTop + child.getMeasuredHeight() + mInsets.top);
|
|
}
|
|
}
|
|
|
|
if (!mHasLayout) {
|
|
mHasLayout = true;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void draw(Canvas c) {
|
|
super.draw(c);
|
|
if (DEBUG) {
|
|
final Paint debugPaint = new Paint();
|
|
debugPaint.setColor(0x40FF00CC);
|
|
// show the isInDragHandle() rect
|
|
c.drawRect(mDragHandleEdgeSlop,
|
|
mChallengeView.getTop() - getDragHandleSizeAbove(),
|
|
getWidth() - mDragHandleEdgeSlop,
|
|
mChallengeView.getTop() + getDragHandleSizeBelow(),
|
|
debugPaint);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
|
|
// Focus security fileds before widgets.
|
|
if (mChallengeView != null &&
|
|
mChallengeView.requestFocus(direction, previouslyFocusedRect)) {
|
|
return true;
|
|
}
|
|
return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
|
|
}
|
|
|
|
public void computeScroll() {
|
|
super.computeScroll();
|
|
|
|
if (!mScroller.isFinished()) {
|
|
if (mChallengeView == null) {
|
|
// Can't scroll if the view is missing.
|
|
Log.e(TAG, "Challenge view missing in computeScroll");
|
|
mScroller.abortAnimation();
|
|
return;
|
|
}
|
|
|
|
mScroller.computeScrollOffset();
|
|
moveChallengeTo(mScroller.getCurrY());
|
|
|
|
if (mScroller.isFinished()) {
|
|
post(mEndScrollRunnable);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void cancelTransitionsInProgress() {
|
|
if (!mScroller.isFinished()) {
|
|
mScroller.abortAnimation();
|
|
completeChallengeScroll();
|
|
}
|
|
if (mFader != null) {
|
|
mFader.cancel();
|
|
}
|
|
}
|
|
|
|
public void fadeInChallenge() {
|
|
fadeChallenge(true);
|
|
}
|
|
|
|
public void fadeOutChallenge() {
|
|
fadeChallenge(false);
|
|
}
|
|
|
|
public void fadeChallenge(final boolean show) {
|
|
if (mChallengeView != null) {
|
|
|
|
cancelTransitionsInProgress();
|
|
float alpha = show ? 1f : 0f;
|
|
int duration = show ? CHALLENGE_FADE_IN_DURATION : CHALLENGE_FADE_OUT_DURATION;
|
|
mFader = ObjectAnimator.ofFloat(mChallengeView, "alpha", alpha);
|
|
mFader.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationStart(Animator animation) {
|
|
onFadeStart(show);
|
|
}
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
onFadeEnd(show);
|
|
}
|
|
});
|
|
mFader.setDuration(duration);
|
|
mFader.start();
|
|
}
|
|
}
|
|
|
|
private int getMaxChallengeBottom() {
|
|
if (mChallengeView == null) return 0;
|
|
final int layoutBottom = getLayoutBottom();
|
|
final int challengeHeight = mChallengeView.getMeasuredHeight();
|
|
|
|
return (layoutBottom + challengeHeight - mChallengeBottomBound);
|
|
}
|
|
|
|
private int getMinChallengeBottom() {
|
|
return getLayoutBottom();
|
|
}
|
|
|
|
|
|
private void onFadeStart(boolean show) {
|
|
mChallengeInteractiveInternal = false;
|
|
enableHardwareLayerForChallengeView();
|
|
|
|
if (show) {
|
|
moveChallengeTo(getMinChallengeBottom());
|
|
}
|
|
|
|
setScrollState(SCROLL_STATE_FADING);
|
|
}
|
|
|
|
private void enableHardwareLayerForChallengeView() {
|
|
if (mChallengeView.isHardwareAccelerated()) {
|
|
mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null);
|
|
}
|
|
}
|
|
|
|
private void onFadeEnd(boolean show) {
|
|
mChallengeInteractiveInternal = true;
|
|
setChallengeShowing(show);
|
|
|
|
if (!show) {
|
|
moveChallengeTo(getMaxChallengeBottom());
|
|
}
|
|
|
|
mChallengeView.setLayerType(LAYER_TYPE_NONE, null);
|
|
mFader = null;
|
|
setScrollState(SCROLL_STATE_IDLE);
|
|
}
|
|
|
|
public int getMaxChallengeTop() {
|
|
if (mChallengeView == null) return 0;
|
|
|
|
final int layoutBottom = getLayoutBottom();
|
|
final int challengeHeight = mChallengeView.getMeasuredHeight();
|
|
return layoutBottom - challengeHeight - mInsets.top;
|
|
}
|
|
|
|
/**
|
|
* Move the bottom edge of mChallengeView to a new position and notify the listener
|
|
* if it represents a change in position. Changes made through this method will
|
|
* be stable across layout passes. If this method is called before first layout of
|
|
* this SlidingChallengeLayout it will have no effect.
|
|
*
|
|
* @param bottom New bottom edge in px in this SlidingChallengeLayout's coordinate system.
|
|
* @return true if the challenge view was moved
|
|
*/
|
|
private boolean moveChallengeTo(int bottom) {
|
|
if (mChallengeView == null || !mHasLayout) {
|
|
return false;
|
|
}
|
|
|
|
final int layoutBottom = getLayoutBottom();
|
|
final int challengeHeight = mChallengeView.getHeight();
|
|
|
|
bottom = Math.max(getMinChallengeBottom(),
|
|
Math.min(bottom, getMaxChallengeBottom()));
|
|
|
|
float offset = 1.f - (float) (bottom - layoutBottom) /
|
|
(challengeHeight - mChallengeBottomBound);
|
|
mChallengeOffset = offset;
|
|
if (offset > 0 && !mChallengeShowing) {
|
|
setChallengeShowing(true);
|
|
}
|
|
|
|
mChallengeView.layout(mChallengeView.getLeft(),
|
|
bottom - mChallengeView.getHeight(), mChallengeView.getRight(), bottom);
|
|
|
|
mChallengeView.setAlpha(getChallengeAlpha());
|
|
if (mScrollListener != null) {
|
|
mScrollListener.onScrollPositionChanged(offset, mChallengeView.getTop());
|
|
}
|
|
postInvalidateOnAnimation();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* The bottom edge of this SlidingChallengeLayout's coordinate system; will coincide with
|
|
* the bottom edge of mChallengeView when the challenge is fully opened.
|
|
*/
|
|
private int getLayoutBottom() {
|
|
final int bottomMargin = (mChallengeView == null)
|
|
? 0
|
|
: ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin;
|
|
final int layoutBottom = getMeasuredHeight() - getPaddingBottom() - bottomMargin
|
|
- mInsets.bottom;
|
|
return layoutBottom;
|
|
}
|
|
|
|
/**
|
|
* The bottom edge of mChallengeView; essentially, where the sliding challenge 'is'.
|
|
*/
|
|
private int getChallengeBottom() {
|
|
if (mChallengeView == null) return 0;
|
|
|
|
return mChallengeView.getBottom();
|
|
}
|
|
|
|
/**
|
|
* Show or hide the challenge view, animating it if necessary.
|
|
* @param show true to show, false to hide
|
|
*/
|
|
public void showChallenge(boolean show) {
|
|
showChallenge(show, 0);
|
|
if (!show) {
|
|
// Block any drags in progress so that callers can use this to disable dragging
|
|
// for other touch interactions.
|
|
mBlockDrag = true;
|
|
}
|
|
}
|
|
|
|
private void showChallenge(int velocity) {
|
|
boolean show = false;
|
|
if (Math.abs(velocity) > mMinVelocity) {
|
|
show = velocity < 0;
|
|
} else {
|
|
show = mChallengeOffset >= 0.5f;
|
|
}
|
|
showChallenge(show, velocity);
|
|
}
|
|
|
|
private void showChallenge(boolean show, int velocity) {
|
|
if (mChallengeView == null) {
|
|
setChallengeShowing(false);
|
|
return;
|
|
}
|
|
|
|
if (mHasLayout) {
|
|
mChallengeShowingTargetState = show;
|
|
final int layoutBottom = getLayoutBottom();
|
|
animateChallengeTo(show ? layoutBottom :
|
|
layoutBottom + mChallengeView.getHeight() - mChallengeBottomBound, velocity);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
|
|
return new LayoutParams(getContext(), attrs);
|
|
}
|
|
|
|
@Override
|
|
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
|
|
return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) :
|
|
p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) :
|
|
new LayoutParams(p);
|
|
}
|
|
|
|
@Override
|
|
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
|
|
return new LayoutParams();
|
|
}
|
|
|
|
@Override
|
|
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
|
|
return p instanceof LayoutParams;
|
|
}
|
|
|
|
public static class LayoutParams extends MarginLayoutParams {
|
|
public int childType = CHILD_TYPE_NONE;
|
|
public static final int CHILD_TYPE_NONE = 0;
|
|
public static final int CHILD_TYPE_CHALLENGE = 2;
|
|
public static final int CHILD_TYPE_SCRIM = 4;
|
|
public static final int CHILD_TYPE_WIDGETS = 5;
|
|
public static final int CHILD_TYPE_EXPAND_CHALLENGE_HANDLE = 6;
|
|
|
|
public int maxHeight;
|
|
|
|
public LayoutParams() {
|
|
this(MATCH_PARENT, WRAP_CONTENT);
|
|
}
|
|
|
|
public LayoutParams(int width, int height) {
|
|
super(width, height);
|
|
}
|
|
|
|
public LayoutParams(android.view.ViewGroup.LayoutParams source) {
|
|
super(source);
|
|
}
|
|
|
|
public LayoutParams(MarginLayoutParams source) {
|
|
super(source);
|
|
}
|
|
|
|
public LayoutParams(LayoutParams source) {
|
|
super(source);
|
|
|
|
childType = source.childType;
|
|
}
|
|
|
|
public LayoutParams(Context c, AttributeSet attrs) {
|
|
super(c, attrs);
|
|
|
|
final TypedArray a = c.obtainStyledAttributes(attrs,
|
|
R.styleable.SlidingChallengeLayout_Layout);
|
|
childType = a.getInt(R.styleable.SlidingChallengeLayout_Layout_layout_childType,
|
|
CHILD_TYPE_NONE);
|
|
maxHeight = a.getDimensionPixelSize(
|
|
R.styleable.SlidingChallengeLayout_Layout_layout_maxHeight, 0);
|
|
a.recycle();
|
|
}
|
|
}
|
|
}
|