MultiPaneChallengeLayout.java revision c065a5d7271c6683b64578490ccc836c72d3ed78
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.policy.impl.keyguard;
18
19import com.android.internal.R;
20
21import android.animation.Animator;
22import android.animation.AnimatorListenerAdapter;
23import android.animation.ObjectAnimator;
24import android.content.Context;
25import android.content.res.Resources;
26import android.content.res.TypedArray;
27import android.graphics.Rect;
28import android.util.AttributeSet;
29import android.util.DisplayMetrics;
30import android.view.Gravity;
31import android.view.View;
32import android.view.ViewGroup;
33import android.widget.LinearLayout;
34
35public class MultiPaneChallengeLayout extends ViewGroup implements ChallengeLayout {
36    private static final String TAG = "MultiPaneChallengeLayout";
37
38    final int mOrientation;
39    private boolean mIsBouncing;
40
41    public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
42    public static final int VERTICAL = LinearLayout.VERTICAL;
43    protected static final int ANIMATE_BOUNCE_DURATION = 750;
44
45    private KeyguardSecurityContainer mChallengeView;
46    private View mUserSwitcherView;
47    private View mScrimView;
48    private OnBouncerStateChangedListener mBouncerListener;
49
50    private final Rect mTempRect = new Rect();
51    private final Rect mZeroPadding = new Rect();
52
53    private final DisplayMetrics mDisplayMetrics;
54
55    private final OnClickListener mScrimClickListener = new OnClickListener() {
56        @Override
57        public void onClick(View v) {
58            hideBouncer();
59        }
60    };
61
62    public MultiPaneChallengeLayout(Context context) {
63        this(context, null);
64    }
65
66    public MultiPaneChallengeLayout(Context context, AttributeSet attrs) {
67        this(context, attrs, 0);
68    }
69
70    public MultiPaneChallengeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
71        super(context, attrs, defStyleAttr);
72
73        final TypedArray a = context.obtainStyledAttributes(attrs,
74                R.styleable.MultiPaneChallengeLayout, defStyleAttr, 0);
75        mOrientation = a.getInt(R.styleable.MultiPaneChallengeLayout_orientation,
76                HORIZONTAL);
77        a.recycle();
78
79        final Resources res = getResources();
80        mDisplayMetrics = res.getDisplayMetrics();
81    }
82
83    @Override
84    public boolean isChallengeShowing() {
85        return true;
86    }
87
88    @Override
89    public boolean isChallengeOverlapping() {
90        return false;
91    }
92
93    @Override
94    public void showChallenge(boolean b) {
95    }
96
97    @Override
98    public void showBouncer() {
99        if (mIsBouncing) return;
100        mIsBouncing = true;
101        if (mScrimView != null) {
102            if (mChallengeView != null) {
103                mChallengeView.showBouncer(ANIMATE_BOUNCE_DURATION);
104            }
105
106            Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 1f);
107            anim.setDuration(ANIMATE_BOUNCE_DURATION);
108            anim.addListener(new AnimatorListenerAdapter() {
109                @Override
110                public void onAnimationStart(Animator animation) {
111                    mScrimView.setVisibility(VISIBLE);
112                }
113            });
114            anim.start();
115        }
116        if (mBouncerListener != null) {
117            mBouncerListener.onBouncerStateChanged(true);
118        }
119    }
120
121    @Override
122    public void hideBouncer() {
123        if (!mIsBouncing) return;
124        mIsBouncing = false;
125        if (mScrimView != null) {
126            if (mChallengeView != null) {
127                mChallengeView.hideBouncer(ANIMATE_BOUNCE_DURATION);
128            }
129
130            Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 0f);
131            anim.setDuration(ANIMATE_BOUNCE_DURATION);
132            anim.addListener(new AnimatorListenerAdapter() {
133                @Override
134                public void onAnimationEnd(Animator animation) {
135                    mScrimView.setVisibility(INVISIBLE);
136                }
137            });
138            anim.start();
139        }
140        if (mBouncerListener != null) {
141            mBouncerListener.onBouncerStateChanged(false);
142        }
143    }
144
145    @Override
146    public boolean isBouncing() {
147        return mIsBouncing;
148    }
149
150    @Override
151    public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) {
152        mBouncerListener = listener;
153    }
154
155    @Override
156    public void requestChildFocus(View child, View focused) {
157        if (mIsBouncing && child != mChallengeView) {
158            // Clear out of the bouncer if the user tries to move focus outside of
159            // the security challenge view.
160            hideBouncer();
161        }
162        super.requestChildFocus(child, focused);
163    }
164
165    void setScrimView(View scrim) {
166        if (mScrimView != null) {
167            mScrimView.setOnClickListener(null);
168        }
169        mScrimView = scrim;
170        mScrimView.setAlpha(mIsBouncing ? 1.0f : 0.0f);
171        mScrimView.setVisibility(mIsBouncing ? VISIBLE : INVISIBLE);
172        mScrimView.setFocusable(true);
173        mScrimView.setOnClickListener(mScrimClickListener);
174    }
175
176    private int getVirtualHeight(LayoutParams lp, int height, int heightUsed) {
177        int virtualHeight = height;
178        final View root = getRootView();
179        if (root != null) {
180            // This calculation is super dodgy and relies on several assumptions.
181            // Specifically that the root of the window will be padded in for insets
182            // and that the window is LAYOUT_IN_SCREEN.
183            virtualHeight = mDisplayMetrics.heightPixels - root.getPaddingTop();
184        }
185        if (lp.childType == LayoutParams.CHILD_TYPE_WIDGET ||
186                lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER) {
187            // Always measure the widget pager/user switcher as if there were no IME insets
188            // on the window. We want to avoid resizing widgets when possible as it can
189            // be ugly/expensive. This lets us simply clip them instead.
190            return virtualHeight - heightUsed;
191        } else if (lp.childType == LayoutParams.CHILD_TYPE_PAGE_DELETE_DROP_TARGET) {
192            return height;
193        }
194        return Math.min(virtualHeight - heightUsed, height);
195    }
196
197    @Override
198    protected void onMeasure(final int widthSpec, final int heightSpec) {
199        if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY ||
200                MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) {
201            throw new IllegalArgumentException(
202                    "MultiPaneChallengeLayout must be measured with an exact size");
203        }
204
205        final int width = MeasureSpec.getSize(widthSpec);
206        final int height = MeasureSpec.getSize(heightSpec);
207        setMeasuredDimension(width, height);
208
209        int widthUsed = 0;
210        int heightUsed = 0;
211
212        // First pass. Find the challenge view and measure the user switcher,
213        // which consumes space in the layout.
214        mChallengeView = null;
215        mUserSwitcherView = null;
216        final int count = getChildCount();
217        for (int i = 0; i < count; i++) {
218            final View child = getChildAt(i);
219            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
220
221            if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) {
222                if (mChallengeView != null) {
223                    throw new IllegalStateException(
224                            "There may only be one child of type challenge");
225                }
226                if (!(child instanceof KeyguardSecurityContainer)) {
227                    throw new IllegalArgumentException(
228                            "Challenge must be a KeyguardSecurityContainer");
229                }
230                mChallengeView = (KeyguardSecurityContainer) child;
231            } else if (lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER) {
232                if (mUserSwitcherView != null) {
233                    throw new IllegalStateException(
234                            "There may only be one child of type userSwitcher");
235                }
236                mUserSwitcherView = child;
237
238                if (child.getVisibility() == GONE) continue;
239
240                int adjustedWidthSpec = widthSpec;
241                int adjustedHeightSpec = heightSpec;
242                if (lp.maxWidth >= 0) {
243                    adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
244                            Math.min(lp.maxWidth, width), MeasureSpec.EXACTLY);
245                }
246                if (lp.maxHeight >= 0) {
247                    adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
248                            Math.min(lp.maxHeight, height), MeasureSpec.EXACTLY);
249                }
250                // measureChildWithMargins will resolve layout direction for the LayoutParams
251                measureChildWithMargins(child, adjustedWidthSpec, 0, adjustedHeightSpec, 0);
252
253                // Only subtract out space from one dimension. Favor vertical.
254                // Offset by 1.5x to add some balance along the other edge.
255                if (Gravity.isVertical(lp.gravity)) {
256                    heightUsed += child.getMeasuredHeight() * 1.5f;
257                } else if (Gravity.isHorizontal(lp.gravity)) {
258                    widthUsed += child.getMeasuredWidth() * 1.5f;
259                }
260            } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) {
261                setScrimView(child);
262                child.measure(widthSpec, heightSpec);
263            }
264        }
265
266        // Second pass. Measure everything that's left.
267        for (int i = 0; i < count; i++) {
268            final View child = getChildAt(i);
269            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
270
271            if (lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER ||
272                    lp.childType == LayoutParams.CHILD_TYPE_SCRIM ||
273                    child.getVisibility() == GONE) {
274                // Don't need to measure GONE children, and the user switcher was already measured.
275                continue;
276            }
277
278            final int virtualHeight = getVirtualHeight(lp, height, heightUsed);
279
280            int adjustedWidthSpec;
281            int adjustedHeightSpec;
282            if (lp.centerWithinArea > 0) {
283                if (mOrientation == HORIZONTAL) {
284                    adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
285                            (int) ((width - widthUsed) * lp.centerWithinArea + 0.5f),
286                            MeasureSpec.EXACTLY);
287                    adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
288                            virtualHeight, MeasureSpec.EXACTLY);
289                } else {
290                    adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
291                            width - widthUsed, MeasureSpec.EXACTLY);
292                    adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
293                            (int) (virtualHeight * lp.centerWithinArea + 0.5f),
294                            MeasureSpec.EXACTLY);
295                }
296            } else {
297                adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
298                        width - widthUsed, MeasureSpec.EXACTLY);
299                adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
300                        virtualHeight, MeasureSpec.EXACTLY);
301            }
302            if (lp.maxWidth >= 0) {
303                adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
304                        Math.min(lp.maxWidth, MeasureSpec.getSize(adjustedWidthSpec)),
305                        MeasureSpec.EXACTLY);
306            }
307            if (lp.maxHeight >= 0) {
308                adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
309                        Math.min(lp.maxHeight, MeasureSpec.getSize(adjustedHeightSpec)),
310                        MeasureSpec.EXACTLY);
311            }
312
313            measureChildWithMargins(child, adjustedWidthSpec, 0, adjustedHeightSpec, 0);
314        }
315    }
316
317    @Override
318    protected void onLayout(boolean changed, int l, int t, int r, int b) {
319        final Rect padding = mTempRect;
320        padding.left = getPaddingLeft();
321        padding.top = getPaddingTop();
322        padding.right = getPaddingRight();
323        padding.bottom = getPaddingBottom();
324        final int width = r - l;
325        final int height = b - t;
326
327        // Reserve extra space in layout for the user switcher by modifying
328        // local padding during this layout pass
329        if (mUserSwitcherView != null && mUserSwitcherView.getVisibility() != GONE) {
330            layoutWithGravity(width, height, mUserSwitcherView, padding, true);
331        }
332
333        final int count = getChildCount();
334        for (int i = 0; i < count; i++) {
335            final View child = getChildAt(i);
336            LayoutParams lp = (LayoutParams) child.getLayoutParams();
337
338            // We did the user switcher above if we have one.
339            if (child == mUserSwitcherView || child.getVisibility() == GONE) continue;
340
341            if (child == mScrimView) {
342                child.layout(0, 0, width, height);
343                continue;
344            } else if (lp.childType == LayoutParams.CHILD_TYPE_PAGE_DELETE_DROP_TARGET) {
345                layoutWithGravity(width, height, child, mZeroPadding, false);
346                continue;
347            }
348
349            layoutWithGravity(width, height, child, padding, false);
350        }
351    }
352
353    private void layoutWithGravity(int width, int height, View child, Rect padding,
354            boolean adjustPadding) {
355        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
356
357        final int heightUsed = padding.top + padding.bottom - getPaddingTop() - getPaddingBottom();
358        height = getVirtualHeight(lp, height, heightUsed);
359
360        final int gravity = Gravity.getAbsoluteGravity(lp.gravity, getLayoutDirection());
361
362        final boolean fixedLayoutSize = lp.centerWithinArea > 0;
363        final boolean fixedLayoutHorizontal = fixedLayoutSize && mOrientation == HORIZONTAL;
364        final boolean fixedLayoutVertical = fixedLayoutSize && mOrientation == VERTICAL;
365
366        final int adjustedWidth;
367        final int adjustedHeight;
368        if (fixedLayoutHorizontal) {
369            final int paddedWidth = width - padding.left - padding.right;
370            adjustedWidth = (int) (paddedWidth * lp.centerWithinArea + 0.5f);
371            adjustedHeight = height;
372        } else if (fixedLayoutVertical) {
373            final int paddedHeight = height - padding.top - padding.bottom;
374            adjustedWidth = width;
375            adjustedHeight = (int) (paddedHeight * lp.centerWithinArea + 0.5f);
376        } else {
377            adjustedWidth = width;
378            adjustedHeight = height;
379        }
380
381        final boolean isVertical = Gravity.isVertical(gravity);
382        final boolean isHorizontal = Gravity.isHorizontal(gravity);
383        final int childWidth = child.getMeasuredWidth();
384        final int childHeight = child.getMeasuredHeight();
385
386        int left = padding.left;
387        int top = padding.top;
388        int right = left + childWidth;
389        int bottom = top + childHeight;
390        switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) {
391            case Gravity.TOP:
392                top = fixedLayoutVertical ?
393                        padding.top + (adjustedHeight - childHeight) / 2 : padding.top;
394                bottom = top + childHeight;
395                if (adjustPadding && isVertical) {
396                    padding.top = bottom;
397                    padding.bottom += childHeight / 2;
398                }
399                break;
400            case Gravity.BOTTOM:
401                bottom = fixedLayoutVertical
402                        ? padding.top + height - (adjustedHeight - childHeight) / 2
403                        : padding.top + height;
404                top = bottom - childHeight;
405                if (adjustPadding && isVertical) {
406                    padding.bottom = height - top;
407                    padding.top += childHeight / 2;
408                }
409                break;
410            case Gravity.CENTER_VERTICAL:
411                top = padding.top + (height - childHeight) / 2;
412                bottom = top + childHeight;
413                break;
414        }
415        switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
416            case Gravity.LEFT:
417                left = fixedLayoutHorizontal ?
418                        padding.left + (adjustedWidth - childWidth) / 2 : padding.left;
419                right = left + childWidth;
420                if (adjustPadding && isHorizontal && !isVertical) {
421                    padding.left = right;
422                    padding.right += childWidth / 2;
423                }
424                break;
425            case Gravity.RIGHT:
426                right = fixedLayoutHorizontal
427                        ? width - padding.right - (adjustedWidth - childWidth) / 2
428                        : width - padding.right;
429                left = right - childWidth;
430                if (adjustPadding && isHorizontal && !isVertical) {
431                    padding.right = width - left;
432                    padding.left += childWidth / 2;
433                }
434                break;
435            case Gravity.CENTER_HORIZONTAL:
436                final int paddedWidth = width - padding.left - padding.right;
437                left = (paddedWidth - childWidth) / 2;
438                right = left + childWidth;
439                break;
440        }
441        child.layout(left, top, right, bottom);
442    }
443
444    @Override
445    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
446        return new LayoutParams(getContext(), attrs, this);
447    }
448
449    @Override
450    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
451        return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) :
452                p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) :
453                new LayoutParams(p);
454    }
455
456    @Override
457    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
458        return new LayoutParams();
459    }
460
461    @Override
462    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
463        return p instanceof LayoutParams;
464    }
465
466    public static class LayoutParams extends MarginLayoutParams {
467
468        public float centerWithinArea = 0;
469
470        public int childType = 0;
471
472        public static final int CHILD_TYPE_NONE = 0;
473        public static final int CHILD_TYPE_WIDGET = 1;
474        public static final int CHILD_TYPE_CHALLENGE = 2;
475        public static final int CHILD_TYPE_USER_SWITCHER = 3;
476        public static final int CHILD_TYPE_SCRIM = 4;
477        public static final int CHILD_TYPE_PAGE_DELETE_DROP_TARGET = 7;
478
479        public int gravity = Gravity.NO_GRAVITY;
480
481        public int maxWidth = -1;
482        public int maxHeight = -1;
483
484        public LayoutParams() {
485            this(WRAP_CONTENT, WRAP_CONTENT);
486        }
487
488        LayoutParams(Context c, AttributeSet attrs, MultiPaneChallengeLayout parent) {
489            super(c, attrs);
490
491            final TypedArray a = c.obtainStyledAttributes(attrs,
492                    R.styleable.MultiPaneChallengeLayout_Layout);
493
494            centerWithinArea = a.getFloat(
495                    R.styleable.MultiPaneChallengeLayout_Layout_layout_centerWithinArea, 0);
496            childType = a.getInt(R.styleable.MultiPaneChallengeLayout_Layout_layout_childType,
497                    CHILD_TYPE_NONE);
498            gravity = a.getInt(R.styleable.MultiPaneChallengeLayout_Layout_layout_gravity,
499                    Gravity.NO_GRAVITY);
500            maxWidth = a.getDimensionPixelSize(
501                    R.styleable.MultiPaneChallengeLayout_Layout_layout_maxWidth, -1);
502            maxHeight = a.getDimensionPixelSize(
503                    R.styleable.MultiPaneChallengeLayout_Layout_layout_maxHeight, -1);
504
505            // Default gravity settings based on type and parent orientation
506            if (gravity == Gravity.NO_GRAVITY) {
507                if (parent.mOrientation == HORIZONTAL) {
508                    switch (childType) {
509                        case CHILD_TYPE_WIDGET:
510                            gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL;
511                            break;
512                        case CHILD_TYPE_CHALLENGE:
513                            gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;
514                            break;
515                        case CHILD_TYPE_USER_SWITCHER:
516                            gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
517                            break;
518                    }
519                } else {
520                    switch (childType) {
521                        case CHILD_TYPE_WIDGET:
522                            gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
523                            break;
524                        case CHILD_TYPE_CHALLENGE:
525                            gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
526                            break;
527                        case CHILD_TYPE_USER_SWITCHER:
528                            gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
529                            break;
530                    }
531                }
532            }
533
534            a.recycle();
535        }
536
537        public LayoutParams(int width, int height) {
538            super(width, height);
539        }
540
541        public LayoutParams(ViewGroup.LayoutParams source) {
542            super(source);
543        }
544
545        public LayoutParams(MarginLayoutParams source) {
546            super(source);
547        }
548
549        public LayoutParams(LayoutParams source) {
550            this((MarginLayoutParams) source);
551
552            centerWithinArea = source.centerWithinArea;
553            childType = source.childType;
554            gravity = source.gravity;
555            maxWidth = source.maxWidth;
556            maxHeight = source.maxHeight;
557        }
558    }
559}
560