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