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_USER_SWITCHER) { 199 // Always measure the user switcher as if there were no IME insets 200 // on the window. 201 return virtualHeight - heightUsed; 202 } else if (lp.childType == LayoutParams.CHILD_TYPE_PAGE_DELETE_DROP_TARGET) { 203 return height; 204 } 205 return Math.min(virtualHeight - heightUsed, height); 206 } 207 208 @Override 209 protected void onMeasure(final int widthSpec, final int heightSpec) { 210 if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY || 211 MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) { 212 throw new IllegalArgumentException( 213 "MultiPaneChallengeLayout must be measured with an exact size"); 214 } 215 216 final int width = MeasureSpec.getSize(widthSpec); 217 final int height = MeasureSpec.getSize(heightSpec); 218 setMeasuredDimension(width, height); 219 220 final int insetHeight = height - mInsets.top - mInsets.bottom; 221 final int insetHeightSpec = MeasureSpec.makeMeasureSpec(insetHeight, MeasureSpec.EXACTLY); 222 223 int widthUsed = 0; 224 int heightUsed = 0; 225 226 // First pass. Find the challenge view and measure the user switcher, 227 // which consumes space in the layout. 228 mChallengeView = null; 229 mUserSwitcherView = null; 230 final int count = getChildCount(); 231 for (int i = 0; i < count; i++) { 232 final View child = getChildAt(i); 233 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 234 235 if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) { 236 if (mChallengeView != null) { 237 throw new IllegalStateException( 238 "There may only be one child of type challenge"); 239 } 240 if (!(child instanceof KeyguardSecurityContainer)) { 241 throw new IllegalArgumentException( 242 "Challenge must be a KeyguardSecurityContainer"); 243 } 244 mChallengeView = (KeyguardSecurityContainer) child; 245 } else if (lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER) { 246 if (mUserSwitcherView != null) { 247 throw new IllegalStateException( 248 "There may only be one child of type userSwitcher"); 249 } 250 mUserSwitcherView = child; 251 252 if (child.getVisibility() == GONE) continue; 253 254 int adjustedWidthSpec = widthSpec; 255 int adjustedHeightSpec = insetHeightSpec; 256 if (lp.maxWidth >= 0) { 257 adjustedWidthSpec = MeasureSpec.makeMeasureSpec( 258 Math.min(lp.maxWidth, width), MeasureSpec.EXACTLY); 259 } 260 if (lp.maxHeight >= 0) { 261 adjustedHeightSpec = MeasureSpec.makeMeasureSpec( 262 Math.min(lp.maxHeight, insetHeight), MeasureSpec.EXACTLY); 263 } 264 // measureChildWithMargins will resolve layout direction for the LayoutParams 265 measureChildWithMargins(child, adjustedWidthSpec, 0, adjustedHeightSpec, 0); 266 267 // Only subtract out space from one dimension. Favor vertical. 268 // Offset by 1.5x to add some balance along the other edge. 269 if (Gravity.isVertical(lp.gravity)) { 270 heightUsed += child.getMeasuredHeight() * 1.5f; 271 } else if (Gravity.isHorizontal(lp.gravity)) { 272 widthUsed += child.getMeasuredWidth() * 1.5f; 273 } 274 } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) { 275 setScrimView(child); 276 child.measure(widthSpec, heightSpec); 277 } 278 } 279 280 // Second pass. Measure everything that's left. 281 for (int i = 0; i < count; i++) { 282 final View child = getChildAt(i); 283 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 284 285 if (lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER || 286 lp.childType == LayoutParams.CHILD_TYPE_SCRIM || 287 child.getVisibility() == GONE) { 288 // Don't need to measure GONE children, and the user switcher was already measured. 289 continue; 290 } 291 292 final int virtualHeight = getVirtualHeight(lp, insetHeight, heightUsed); 293 294 int adjustedWidthSpec; 295 int adjustedHeightSpec; 296 if (lp.centerWithinArea > 0) { 297 if (mOrientation == HORIZONTAL) { 298 adjustedWidthSpec = MeasureSpec.makeMeasureSpec( 299 (int) ((width - widthUsed) * lp.centerWithinArea + 0.5f), 300 MeasureSpec.EXACTLY); 301 adjustedHeightSpec = MeasureSpec.makeMeasureSpec( 302 virtualHeight, MeasureSpec.EXACTLY); 303 } else { 304 adjustedWidthSpec = MeasureSpec.makeMeasureSpec( 305 width - widthUsed, MeasureSpec.EXACTLY); 306 adjustedHeightSpec = MeasureSpec.makeMeasureSpec( 307 (int) (virtualHeight * lp.centerWithinArea + 0.5f), 308 MeasureSpec.EXACTLY); 309 } 310 } else { 311 adjustedWidthSpec = MeasureSpec.makeMeasureSpec( 312 width - widthUsed, MeasureSpec.EXACTLY); 313 adjustedHeightSpec = MeasureSpec.makeMeasureSpec( 314 virtualHeight, MeasureSpec.EXACTLY); 315 } 316 if (lp.maxWidth >= 0) { 317 adjustedWidthSpec = MeasureSpec.makeMeasureSpec( 318 Math.min(lp.maxWidth, MeasureSpec.getSize(adjustedWidthSpec)), 319 MeasureSpec.EXACTLY); 320 } 321 if (lp.maxHeight >= 0) { 322 adjustedHeightSpec = MeasureSpec.makeMeasureSpec( 323 Math.min(lp.maxHeight, MeasureSpec.getSize(adjustedHeightSpec)), 324 MeasureSpec.EXACTLY); 325 } 326 327 measureChildWithMargins(child, adjustedWidthSpec, 0, adjustedHeightSpec, 0); 328 } 329 } 330 331 @Override 332 protected void onLayout(boolean changed, int l, int t, int r, int b) { 333 final Rect padding = mTempRect; 334 padding.left = getPaddingLeft(); 335 padding.top = getPaddingTop(); 336 padding.right = getPaddingRight(); 337 padding.bottom = getPaddingBottom(); 338 final int width = r - l; 339 final int height = b - t; 340 final int insetHeight = height - mInsets.top - mInsets.bottom; 341 342 // Reserve extra space in layout for the user switcher by modifying 343 // local padding during this layout pass 344 if (mUserSwitcherView != null && mUserSwitcherView.getVisibility() != GONE) { 345 layoutWithGravity(width, insetHeight, mUserSwitcherView, padding, true); 346 } 347 348 final int count = getChildCount(); 349 for (int i = 0; i < count; i++) { 350 final View child = getChildAt(i); 351 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 352 353 // We did the user switcher above if we have one. 354 if (child == mUserSwitcherView || child.getVisibility() == GONE) continue; 355 356 if (child == mScrimView) { 357 child.layout(0, 0, width, height); 358 continue; 359 } else if (lp.childType == LayoutParams.CHILD_TYPE_PAGE_DELETE_DROP_TARGET) { 360 layoutWithGravity(width, insetHeight, child, mZeroPadding, false); 361 continue; 362 } 363 364 layoutWithGravity(width, insetHeight, child, padding, false); 365 } 366 } 367 368 private void layoutWithGravity(int width, int height, View child, Rect padding, 369 boolean adjustPadding) { 370 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 371 372 final int heightUsed = padding.top + padding.bottom - getPaddingTop() - getPaddingBottom(); 373 height = getVirtualHeight(lp, height, heightUsed); 374 375 final int gravity = Gravity.getAbsoluteGravity(lp.gravity, getLayoutDirection()); 376 377 final boolean fixedLayoutSize = lp.centerWithinArea > 0; 378 final boolean fixedLayoutHorizontal = fixedLayoutSize && mOrientation == HORIZONTAL; 379 final boolean fixedLayoutVertical = fixedLayoutSize && mOrientation == VERTICAL; 380 381 final int adjustedWidth; 382 final int adjustedHeight; 383 if (fixedLayoutHorizontal) { 384 final int paddedWidth = width - padding.left - padding.right; 385 adjustedWidth = (int) (paddedWidth * lp.centerWithinArea + 0.5f); 386 adjustedHeight = height; 387 } else if (fixedLayoutVertical) { 388 final int paddedHeight = height - getPaddingTop() - getPaddingBottom(); 389 adjustedWidth = width; 390 adjustedHeight = (int) (paddedHeight * lp.centerWithinArea + 0.5f); 391 } else { 392 adjustedWidth = width; 393 adjustedHeight = height; 394 } 395 396 final boolean isVertical = Gravity.isVertical(gravity); 397 final boolean isHorizontal = Gravity.isHorizontal(gravity); 398 final int childWidth = child.getMeasuredWidth(); 399 final int childHeight = child.getMeasuredHeight(); 400 401 int left = padding.left; 402 int top = padding.top; 403 int right = left + childWidth; 404 int bottom = top + childHeight; 405 switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) { 406 case Gravity.TOP: 407 top = fixedLayoutVertical ? 408 padding.top + (adjustedHeight - childHeight) / 2 : padding.top; 409 bottom = top + childHeight; 410 if (adjustPadding && isVertical) { 411 padding.top = bottom; 412 padding.bottom += childHeight / 2; 413 } 414 break; 415 case Gravity.BOTTOM: 416 bottom = fixedLayoutVertical 417 ? padding.top + height - (adjustedHeight - childHeight) / 2 418 : padding.top + height; 419 top = bottom - childHeight; 420 if (adjustPadding && isVertical) { 421 padding.bottom = height - top; 422 padding.top += childHeight / 2; 423 } 424 break; 425 case Gravity.CENTER_VERTICAL: 426 top = padding.top + (height - childHeight) / 2; 427 bottom = top + childHeight; 428 break; 429 } 430 switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 431 case Gravity.LEFT: 432 left = fixedLayoutHorizontal ? 433 padding.left + (adjustedWidth - childWidth) / 2 : padding.left; 434 right = left + childWidth; 435 if (adjustPadding && isHorizontal && !isVertical) { 436 padding.left = right; 437 padding.right += childWidth / 2; 438 } 439 break; 440 case Gravity.RIGHT: 441 right = fixedLayoutHorizontal 442 ? width - padding.right - (adjustedWidth - childWidth) / 2 443 : width - padding.right; 444 left = right - childWidth; 445 if (adjustPadding && isHorizontal && !isVertical) { 446 padding.right = width - left; 447 padding.left += childWidth / 2; 448 } 449 break; 450 case Gravity.CENTER_HORIZONTAL: 451 final int paddedWidth = width - padding.left - padding.right; 452 left = (paddedWidth - childWidth) / 2; 453 right = left + childWidth; 454 break; 455 } 456 top += mInsets.top; 457 bottom += mInsets.top; 458 child.layout(left, top, right, bottom); 459 } 460 461 @Override 462 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 463 return new LayoutParams(getContext(), attrs, this); 464 } 465 466 @Override 467 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 468 return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) : 469 p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : 470 new LayoutParams(p); 471 } 472 473 @Override 474 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 475 return new LayoutParams(); 476 } 477 478 @Override 479 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 480 return p instanceof LayoutParams; 481 } 482 483 public static class LayoutParams extends MarginLayoutParams { 484 485 public float centerWithinArea = 0; 486 487 public int childType = 0; 488 489 public static final int CHILD_TYPE_NONE = 0; 490 public static final int CHILD_TYPE_WIDGET = 1; 491 public static final int CHILD_TYPE_CHALLENGE = 2; 492 public static final int CHILD_TYPE_USER_SWITCHER = 3; 493 public static final int CHILD_TYPE_SCRIM = 4; 494 public static final int CHILD_TYPE_PAGE_DELETE_DROP_TARGET = 7; 495 496 public int gravity = Gravity.NO_GRAVITY; 497 498 public int maxWidth = -1; 499 public int maxHeight = -1; 500 501 public LayoutParams() { 502 this(WRAP_CONTENT, WRAP_CONTENT); 503 } 504 505 LayoutParams(Context c, AttributeSet attrs, MultiPaneChallengeLayout parent) { 506 super(c, attrs); 507 508 final TypedArray a = c.obtainStyledAttributes(attrs, 509 R.styleable.MultiPaneChallengeLayout_Layout); 510 511 centerWithinArea = a.getFloat( 512 R.styleable.MultiPaneChallengeLayout_Layout_layout_centerWithinArea, 0); 513 childType = a.getInt(R.styleable.MultiPaneChallengeLayout_Layout_layout_childType, 514 CHILD_TYPE_NONE); 515 gravity = a.getInt(R.styleable.MultiPaneChallengeLayout_Layout_layout_gravity, 516 Gravity.NO_GRAVITY); 517 maxWidth = a.getDimensionPixelSize( 518 R.styleable.MultiPaneChallengeLayout_Layout_layout_maxWidth, -1); 519 maxHeight = a.getDimensionPixelSize( 520 R.styleable.MultiPaneChallengeLayout_Layout_layout_maxHeight, -1); 521 522 // Default gravity settings based on type and parent orientation 523 if (gravity == Gravity.NO_GRAVITY) { 524 if (parent.mOrientation == HORIZONTAL) { 525 switch (childType) { 526 case CHILD_TYPE_WIDGET: 527 gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL; 528 break; 529 case CHILD_TYPE_CHALLENGE: 530 gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL; 531 break; 532 case CHILD_TYPE_USER_SWITCHER: 533 gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; 534 break; 535 } 536 } else { 537 switch (childType) { 538 case CHILD_TYPE_WIDGET: 539 gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; 540 break; 541 case CHILD_TYPE_CHALLENGE: 542 gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; 543 break; 544 case CHILD_TYPE_USER_SWITCHER: 545 gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; 546 break; 547 } 548 } 549 } 550 551 a.recycle(); 552 } 553 554 public LayoutParams(int width, int height) { 555 super(width, height); 556 } 557 558 public LayoutParams(ViewGroup.LayoutParams source) { 559 super(source); 560 } 561 562 public LayoutParams(MarginLayoutParams source) { 563 super(source); 564 } 565 566 public LayoutParams(LayoutParams source) { 567 this((MarginLayoutParams) source); 568 569 centerWithinArea = source.centerWithinArea; 570 childType = source.childType; 571 gravity = source.gravity; 572 maxWidth = source.maxWidth; 573 maxHeight = source.maxHeight; 574 } 575 } 576} 577