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