blob: 340a4d5df820f5ce0962a6111b5d78eae6ace58d [file] [log] [blame]
/*
* 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.Rect;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
public class MultiPaneChallengeLayout extends ViewGroup implements ChallengeLayout {
private static final String TAG = "MultiPaneChallengeLayout";
final int mOrientation;
private boolean mIsBouncing;
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;
public static final int ANIMATE_BOUNCE_DURATION = 350;
private KeyguardSecurityContainer mChallengeView;
private View mUserSwitcherView;
private View mScrimView;
private OnBouncerStateChangedListener mBouncerListener;
private final Rect mTempRect = new Rect();
private final Rect mZeroPadding = new Rect();
private final Rect mInsets = new Rect();
private final DisplayMetrics mDisplayMetrics;
private final OnClickListener mScrimClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
hideBouncer();
}
};
public MultiPaneChallengeLayout(Context context) {
this(context, null);
}
public MultiPaneChallengeLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MultiPaneChallengeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.MultiPaneChallengeLayout, defStyleAttr, 0);
mOrientation = a.getInt(R.styleable.MultiPaneChallengeLayout_android_orientation,
HORIZONTAL);
a.recycle();
final Resources res = getResources();
mDisplayMetrics = res.getDisplayMetrics();
setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
}
public void setInsets(Rect insets) {
mInsets.set(insets);
}
@Override
public boolean isChallengeShowing() {
return true;
}
@Override
public boolean isChallengeOverlapping() {
return false;
}
@Override
public void showChallenge(boolean b) {
}
@Override
public int getBouncerAnimationDuration() {
return ANIMATE_BOUNCE_DURATION;
}
@Override
public void showBouncer() {
if (mIsBouncing) return;
mIsBouncing = true;
if (mScrimView != null) {
if (mChallengeView != null) {
mChallengeView.showBouncer(ANIMATE_BOUNCE_DURATION);
}
Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 1f);
anim.setDuration(ANIMATE_BOUNCE_DURATION);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mScrimView.setVisibility(VISIBLE);
}
});
anim.start();
}
if (mBouncerListener != null) {
mBouncerListener.onBouncerStateChanged(true);
}
}
@Override
public void hideBouncer() {
if (!mIsBouncing) return;
mIsBouncing = false;
if (mScrimView != null) {
if (mChallengeView != null) {
mChallengeView.hideBouncer(ANIMATE_BOUNCE_DURATION);
}
Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 0f);
anim.setDuration(ANIMATE_BOUNCE_DURATION);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mScrimView.setVisibility(INVISIBLE);
}
});
anim.start();
}
if (mBouncerListener != null) {
mBouncerListener.onBouncerStateChanged(false);
}
}
@Override
public boolean isBouncing() {
return mIsBouncing;
}
@Override
public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) {
mBouncerListener = listener;
}
@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);
}
void setScrimView(View scrim) {
if (mScrimView != null) {
mScrimView.setOnClickListener(null);
}
mScrimView = scrim;
if (mScrimView != null) {
mScrimView.setAlpha(mIsBouncing ? 1.0f : 0.0f);
mScrimView.setVisibility(mIsBouncing ? VISIBLE : INVISIBLE);
mScrimView.setFocusable(true);
mScrimView.setOnClickListener(mScrimClickListener);
}
}
private int getVirtualHeight(LayoutParams lp, int height, int heightUsed) {
int virtualHeight = height;
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.
virtualHeight = mDisplayMetrics.heightPixels - root.getPaddingTop() - mInsets.top;
}
if (lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER) {
// Always measure the user switcher as if there were no IME insets
// on the window.
return virtualHeight - heightUsed;
} else if (lp.childType == LayoutParams.CHILD_TYPE_PAGE_DELETE_DROP_TARGET) {
return height;
}
return Math.min(virtualHeight - heightUsed, height);
}
@Override
protected void onMeasure(final int widthSpec, final int heightSpec) {
if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) {
throw new IllegalArgumentException(
"MultiPaneChallengeLayout 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);
int widthUsed = 0;
int heightUsed = 0;
// First pass. Find the challenge view and measure the user switcher,
// which consumes space in the layout.
mChallengeView = null;
mUserSwitcherView = null;
final int count = getChildCount();
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 of type challenge");
}
if (!(child instanceof KeyguardSecurityContainer)) {
throw new IllegalArgumentException(
"Challenge must be a KeyguardSecurityContainer");
}
mChallengeView = (KeyguardSecurityContainer) child;
} else if (lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER) {
if (mUserSwitcherView != null) {
throw new IllegalStateException(
"There may only be one child of type userSwitcher");
}
mUserSwitcherView = child;
if (child.getVisibility() == GONE) continue;
int adjustedWidthSpec = widthSpec;
int adjustedHeightSpec = insetHeightSpec;
if (lp.maxWidth >= 0) {
adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
Math.min(lp.maxWidth, width), MeasureSpec.EXACTLY);
}
if (lp.maxHeight >= 0) {
adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
Math.min(lp.maxHeight, insetHeight), MeasureSpec.EXACTLY);
}
// measureChildWithMargins will resolve layout direction for the LayoutParams
measureChildWithMargins(child, adjustedWidthSpec, 0, adjustedHeightSpec, 0);
// Only subtract out space from one dimension. Favor vertical.
// Offset by 1.5x to add some balance along the other edge.
if (Gravity.isVertical(lp.gravity)) {
heightUsed += child.getMeasuredHeight() * 1.5f;
} else if (Gravity.isHorizontal(lp.gravity)) {
widthUsed += child.getMeasuredWidth() * 1.5f;
}
} else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) {
setScrimView(child);
child.measure(widthSpec, heightSpec);
}
}
// Second pass. Measure everything that's left.
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER ||
lp.childType == LayoutParams.CHILD_TYPE_SCRIM ||
child.getVisibility() == GONE) {
// Don't need to measure GONE children, and the user switcher was already measured.
continue;
}
final int virtualHeight = getVirtualHeight(lp, insetHeight, heightUsed);
int adjustedWidthSpec;
int adjustedHeightSpec;
if (lp.centerWithinArea > 0) {
if (mOrientation == HORIZONTAL) {
adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
(int) ((width - widthUsed) * lp.centerWithinArea + 0.5f),
MeasureSpec.EXACTLY);
adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
virtualHeight, MeasureSpec.EXACTLY);
} else {
adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
width - widthUsed, MeasureSpec.EXACTLY);
adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
(int) (virtualHeight * lp.centerWithinArea + 0.5f),
MeasureSpec.EXACTLY);
}
} else {
adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
width - widthUsed, MeasureSpec.EXACTLY);
adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
virtualHeight, MeasureSpec.EXACTLY);
}
if (lp.maxWidth >= 0) {
adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
Math.min(lp.maxWidth, MeasureSpec.getSize(adjustedWidthSpec)),
MeasureSpec.EXACTLY);
}
if (lp.maxHeight >= 0) {
adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
Math.min(lp.maxHeight, MeasureSpec.getSize(adjustedHeightSpec)),
MeasureSpec.EXACTLY);
}
measureChildWithMargins(child, adjustedWidthSpec, 0, adjustedHeightSpec, 0);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final Rect padding = mTempRect;
padding.left = getPaddingLeft();
padding.top = getPaddingTop();
padding.right = getPaddingRight();
padding.bottom = getPaddingBottom();
final int width = r - l;
final int height = b - t;
final int insetHeight = height - mInsets.top - mInsets.bottom;
// Reserve extra space in layout for the user switcher by modifying
// local padding during this layout pass
if (mUserSwitcherView != null && mUserSwitcherView.getVisibility() != GONE) {
layoutWithGravity(width, insetHeight, mUserSwitcherView, padding, true);
}
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
// We did the user switcher above if we have one.
if (child == mUserSwitcherView || child.getVisibility() == GONE) continue;
if (child == mScrimView) {
child.layout(0, 0, width, height);
continue;
} else if (lp.childType == LayoutParams.CHILD_TYPE_PAGE_DELETE_DROP_TARGET) {
layoutWithGravity(width, insetHeight, child, mZeroPadding, false);
continue;
}
layoutWithGravity(width, insetHeight, child, padding, false);
}
}
private void layoutWithGravity(int width, int height, View child, Rect padding,
boolean adjustPadding) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int heightUsed = padding.top + padding.bottom - getPaddingTop() - getPaddingBottom();
height = getVirtualHeight(lp, height, heightUsed);
final int gravity = Gravity.getAbsoluteGravity(lp.gravity, getLayoutDirection());
final boolean fixedLayoutSize = lp.centerWithinArea > 0;
final boolean fixedLayoutHorizontal = fixedLayoutSize && mOrientation == HORIZONTAL;
final boolean fixedLayoutVertical = fixedLayoutSize && mOrientation == VERTICAL;
final int adjustedWidth;
final int adjustedHeight;
if (fixedLayoutHorizontal) {
final int paddedWidth = width - padding.left - padding.right;
adjustedWidth = (int) (paddedWidth * lp.centerWithinArea + 0.5f);
adjustedHeight = height;
} else if (fixedLayoutVertical) {
final int paddedHeight = height - getPaddingTop() - getPaddingBottom();
adjustedWidth = width;
adjustedHeight = (int) (paddedHeight * lp.centerWithinArea + 0.5f);
} else {
adjustedWidth = width;
adjustedHeight = height;
}
final boolean isVertical = Gravity.isVertical(gravity);
final boolean isHorizontal = Gravity.isHorizontal(gravity);
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
int left = padding.left;
int top = padding.top;
int right = left + childWidth;
int bottom = top + childHeight;
switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) {
case Gravity.TOP:
top = fixedLayoutVertical ?
padding.top + (adjustedHeight - childHeight) / 2 : padding.top;
bottom = top + childHeight;
if (adjustPadding && isVertical) {
padding.top = bottom;
padding.bottom += childHeight / 2;
}
break;
case Gravity.BOTTOM:
bottom = fixedLayoutVertical
? padding.top + height - (adjustedHeight - childHeight) / 2
: padding.top + height;
top = bottom - childHeight;
if (adjustPadding && isVertical) {
padding.bottom = height - top;
padding.top += childHeight / 2;
}
break;
case Gravity.CENTER_VERTICAL:
top = padding.top + (height - childHeight) / 2;
bottom = top + childHeight;
break;
}
switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.LEFT:
left = fixedLayoutHorizontal ?
padding.left + (adjustedWidth - childWidth) / 2 : padding.left;
right = left + childWidth;
if (adjustPadding && isHorizontal && !isVertical) {
padding.left = right;
padding.right += childWidth / 2;
}
break;
case Gravity.RIGHT:
right = fixedLayoutHorizontal
? width - padding.right - (adjustedWidth - childWidth) / 2
: width - padding.right;
left = right - childWidth;
if (adjustPadding && isHorizontal && !isVertical) {
padding.right = width - left;
padding.left += childWidth / 2;
}
break;
case Gravity.CENTER_HORIZONTAL:
final int paddedWidth = width - padding.left - padding.right;
left = (paddedWidth - childWidth) / 2;
right = left + childWidth;
break;
}
top += mInsets.top;
bottom += mInsets.top;
child.layout(left, top, right, bottom);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs, this);
}
@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 float centerWithinArea = 0;
public int childType = 0;
public static final int CHILD_TYPE_NONE = 0;
public static final int CHILD_TYPE_WIDGET = 1;
public static final int CHILD_TYPE_CHALLENGE = 2;
public static final int CHILD_TYPE_USER_SWITCHER = 3;
public static final int CHILD_TYPE_SCRIM = 4;
public static final int CHILD_TYPE_PAGE_DELETE_DROP_TARGET = 7;
public int gravity = Gravity.NO_GRAVITY;
public int maxWidth = -1;
public int maxHeight = -1;
public LayoutParams() {
this(WRAP_CONTENT, WRAP_CONTENT);
}
LayoutParams(Context c, AttributeSet attrs, MultiPaneChallengeLayout parent) {
super(c, attrs);
final TypedArray a = c.obtainStyledAttributes(attrs,
R.styleable.MultiPaneChallengeLayout_Layout);
centerWithinArea = a.getFloat(
R.styleable.MultiPaneChallengeLayout_Layout_layout_centerWithinArea, 0);
childType = a.getInt(R.styleable.MultiPaneChallengeLayout_Layout_layout_childType,
CHILD_TYPE_NONE);
gravity = a.getInt(R.styleable.MultiPaneChallengeLayout_Layout_layout_gravity,
Gravity.NO_GRAVITY);
maxWidth = a.getDimensionPixelSize(
R.styleable.MultiPaneChallengeLayout_Layout_layout_maxWidth, -1);
maxHeight = a.getDimensionPixelSize(
R.styleable.MultiPaneChallengeLayout_Layout_layout_maxHeight, -1);
// Default gravity settings based on type and parent orientation
if (gravity == Gravity.NO_GRAVITY) {
if (parent.mOrientation == HORIZONTAL) {
switch (childType) {
case CHILD_TYPE_WIDGET:
gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL;
break;
case CHILD_TYPE_CHALLENGE:
gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;
break;
case CHILD_TYPE_USER_SWITCHER:
gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
break;
}
} else {
switch (childType) {
case CHILD_TYPE_WIDGET:
gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
break;
case CHILD_TYPE_CHALLENGE:
gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
break;
case CHILD_TYPE_USER_SWITCHER:
gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
break;
}
}
}
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(LayoutParams source) {
this((MarginLayoutParams) source);
centerWithinArea = source.centerWithinArea;
childType = source.childType;
gravity = source.gravity;
maxWidth = source.maxWidth;
maxHeight = source.maxHeight;
}
}
}