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