SlidingChallengeLayout.java revision 86b6357e5eb91950eac7de7ffe29e5a4ad32903b
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 android.content.Context; 20import android.content.res.TypedArray; 21import android.graphics.Canvas; 22import android.graphics.drawable.Drawable; 23import android.util.AttributeSet; 24import android.util.Log; 25import android.view.MotionEvent; 26import android.view.VelocityTracker; 27import android.view.View; 28import android.view.ViewConfiguration; 29import android.view.ViewGroup; 30import android.view.animation.Interpolator; 31import android.widget.Scroller; 32 33import com.android.internal.R; 34 35/** 36 * This layout handles interaction with the sliding security challenge views 37 * that overlay/resize other keyguard contents. 38 */ 39public class SlidingChallengeLayout extends ViewGroup implements ChallengeLayout { 40 private static final String TAG = "SlidingChallengeLayout"; 41 42 // Drawn to show the drag handle in closed state; crossfades to the challenge view 43 // when challenge is fully visible 44 private Drawable mHandleDrawable; 45 private boolean mShowHandle = true; 46 47 // Initialized during measurement from child layoutparams 48 private View mChallengeView; 49 50 // Range: 0 (fully hidden) to 1 (fully visible) 51 private float mChallengeOffset = 1.f; 52 private boolean mChallengeShowing = true; 53 54 private final Scroller mScroller; 55 private int mScrollState; 56 private OnChallengeScrolledListener mListener; 57 58 public static final int SCROLL_STATE_IDLE = 0; 59 public static final int SCROLL_STATE_DRAGGING = 1; 60 public static final int SCROLL_STATE_SETTLING = 2; 61 62 private static final int MAX_SETTLE_DURATION = 600; // ms 63 64 // ID of the pointer in charge of a current drag 65 private int mActivePointerId = INVALID_POINTER; 66 private static final int INVALID_POINTER = -1; 67 68 // True if the user is currently dragging the slider 69 private boolean mDragging; 70 // True if the user may not drag until a new gesture begins 71 private boolean mBlockDrag; 72 73 private VelocityTracker mVelocityTracker; 74 private int mMinVelocity; 75 private int mMaxVelocity; 76 private float mLastTouchY; 77 private int mDragHandleSize; 78 79 private static final int DRAG_HANDLE_DEFAULT_SIZE = 32; // dp 80 81 // True if at least one layout pass has happened since the view was attached. 82 private boolean mHasLayout; 83 84 private static final Interpolator sMotionInterpolator = new Interpolator() { 85 public float getInterpolation(float t) { 86 t -= 1.0f; 87 return t * t * t * t * t + 1.0f; 88 } 89 }; 90 91 private static final Interpolator sHandleFadeInterpolator = new Interpolator() { 92 public float getInterpolation(float t) { 93 return t * t; 94 } 95 }; 96 97 private final Runnable mEndScrollRunnable = new Runnable () { 98 public void run() { 99 completeChallengeScroll(); 100 } 101 }; 102 103 /** 104 * Listener interface that reports changes in scroll state of the challenge area. 105 */ 106 public interface OnChallengeScrolledListener { 107 /** 108 * The scroll state itself changed. 109 * 110 * <p>scrollState will be one of the following:</p> 111 * 112 * <ul> 113 * <li><code>SCROLL_STATE_IDLE</code> - The challenge area is stationary.</li> 114 * <li><code>SCROLL_STATE_DRAGGING</code> - The user is actively dragging 115 * the challenge area.</li> 116 * <li><code>SCROLL_STATE_SETTLING</code> - The challenge area is animating 117 * into place.</li> 118 * </ul> 119 * 120 * <p>Do not perform expensive operations (e.g. layout) 121 * while the scroll state is not <code>SCROLL_STATE_IDLE</code>.</p> 122 * 123 * @param scrollState The new scroll state of the challenge area. 124 */ 125 public void onScrollStateChanged(int scrollState); 126 127 /** 128 * The precise position of the challenge area has changed. 129 * 130 * <p>NOTE: It is NOT safe to modify layout or call any View methods that may 131 * result in a requestLayout anywhere in your view hierarchy as a result of this call. 132 * It may be called during drawing.</p> 133 * 134 * @param scrollPosition New relative position of the challenge area. 135 * 1.f = fully visible/ready to be interacted with. 136 * 0.f = fully invisible/inaccessible to the user. 137 * @param challengeTop Position of the top edge of the challenge view in px in the 138 * SlidingChallengeLayout's coordinate system. 139 */ 140 public void onScrollPositionChanged(float scrollPosition, int challengeTop); 141 } 142 143 public SlidingChallengeLayout(Context context) { 144 this(context, null); 145 } 146 147 public SlidingChallengeLayout(Context context, AttributeSet attrs) { 148 this(context, attrs, 0); 149 } 150 151 public SlidingChallengeLayout(Context context, AttributeSet attrs, int defStyle) { 152 super(context, attrs, defStyle); 153 154 final TypedArray a = context.obtainStyledAttributes(attrs, 155 R.styleable.SlidingChallengeLayout, defStyle, 0); 156 setDragHandleDrawable(a.getDrawable(R.styleable.SlidingChallengeLayout_dragHandle)); 157 158 a.recycle(); 159 160 mScroller = new Scroller(context, sMotionInterpolator); 161 162 final ViewConfiguration vc = ViewConfiguration.get(context); 163 mMinVelocity = vc.getScaledMinimumFlingVelocity(); 164 mMaxVelocity = vc.getScaledMaximumFlingVelocity(); 165 166 setWillNotDraw(false); 167 } 168 169 public void setDragHandleDrawable(Drawable d) { 170 if (d != null) { 171 mDragHandleSize = d.getIntrinsicHeight(); 172 } 173 if (mDragHandleSize == 0 || d == null) { 174 final float density = getResources().getDisplayMetrics().density; 175 mDragHandleSize = (int) (DRAG_HANDLE_DEFAULT_SIZE * density + 0.5f); 176 } 177 mHandleDrawable = d; 178 } 179 180 private void sendInitialListenerUpdates() { 181 if (mListener != null) { 182 int challengeTop = mChallengeView != null ? mChallengeView.getTop() : 0; 183 mListener.onScrollPositionChanged(mChallengeOffset, challengeTop); 184 mListener.onScrollStateChanged(mScrollState); 185 } 186 } 187 188 public void setOnChallengeScrolledListener(OnChallengeScrolledListener listener) { 189 mListener = listener; 190 if (mHasLayout) { 191 sendInitialListenerUpdates(); 192 } 193 } 194 195 @Override 196 public void onAttachedToWindow() { 197 super.onAttachedToWindow(); 198 199 mHasLayout = false; 200 } 201 202 @Override 203 public void onDetachedFromWindow() { 204 super.onDetachedFromWindow(); 205 206 removeCallbacks(mEndScrollRunnable); 207 mHasLayout = false; 208 } 209 210 // We want the duration of the page snap animation to be influenced by the distance that 211 // the screen has to travel, however, we don't want this duration to be effected in a 212 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 213 // of travel has on the overall snap duration. 214 float distanceInfluenceForSnapDuration(float f) { 215 f -= 0.5f; // center the values about 0. 216 f *= 0.3f * Math.PI / 2.0f; 217 return (float) Math.sin(f); 218 } 219 220 void setScrollState(int state) { 221 if (mScrollState != state) { 222 mScrollState = state; 223 224 if (mListener != null) { 225 mListener.onScrollStateChanged(state); 226 } 227 } 228 } 229 230 void completeChallengeScroll() { 231 setChallengeShowing(mChallengeOffset != 0); 232 setScrollState(SCROLL_STATE_IDLE); 233 } 234 235 /** 236 * Animate the bottom edge of the challenge view to the given position. 237 * 238 * @param y desired final position for the bottom edge of the challenge view in px 239 * @param velocity velocity in 240 */ 241 void animateChallengeTo(int y, int velocity) { 242 if (mChallengeView == null) { 243 // Nothing to do. 244 return; 245 } 246 int sy = mChallengeView.getBottom(); 247 int dy = y - sy; 248 if (dy == 0) { 249 completeChallengeScroll(); 250 return; 251 } 252 253 setScrollState(SCROLL_STATE_SETTLING); 254 255 final int childHeight = mChallengeView.getHeight(); 256 final int halfHeight = childHeight / 2; 257 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / childHeight); 258 final float distance = halfHeight + halfHeight * 259 distanceInfluenceForSnapDuration(distanceRatio); 260 261 int duration = 0; 262 velocity = Math.abs(velocity); 263 if (velocity > 0) { 264 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 265 } else { 266 final float childDelta = (float) Math.abs(dy) / childHeight; 267 duration = (int) ((childDelta + 1) * 100); 268 } 269 duration = Math.min(duration, MAX_SETTLE_DURATION); 270 271 mScroller.startScroll(0, sy, 0, dy, duration); 272 postInvalidateOnAnimation(); 273 } 274 275 private void setChallengeShowing(boolean showChallenge) { 276 if (mChallengeShowing != showChallenge) { 277 mChallengeShowing = showChallenge; 278 if (mChallengeView != null) { 279 mChallengeView.setVisibility(showChallenge ? VISIBLE : INVISIBLE); 280 } 281 } 282 } 283 284 /** 285 * @return true if the challenge is at all visible. 286 */ 287 public boolean isChallengeShowing() { 288 return mChallengeShowing; 289 } 290 291 @Override 292 public boolean isChallengeOverlapping() { 293 return mChallengeShowing; 294 } 295 296 @Override 297 public void requestDisallowInterceptTouchEvent(boolean allowIntercept) { 298 // We'll intercept whoever we feel like! ...as long as it isn't a challenge view. 299 // If there are one or more pointers in the challenge view before we take over 300 // touch events, onInterceptTouchEvent will set mBlockDrag. 301 } 302 303 @Override 304 public boolean onInterceptTouchEvent(MotionEvent ev) { 305 if (mVelocityTracker == null) { 306 mVelocityTracker = VelocityTracker.obtain(); 307 } 308 mVelocityTracker.addMovement(ev); 309 310 final int action = ev.getActionMasked(); 311 switch (action) { 312 case MotionEvent.ACTION_DOWN: 313 mLastTouchY = ev.getY(); 314 break; 315 316 case MotionEvent.ACTION_CANCEL: 317 case MotionEvent.ACTION_UP: 318 resetTouch(); 319 break; 320 321 case MotionEvent.ACTION_MOVE: 322 final int count = ev.getPointerCount(); 323 for (int i = 0; i < count; i++) { 324 final float x = ev.getX(i); 325 final float y = ev.getY(i); 326 327 if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mLastTouchY) || 328 (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING)) && 329 mActivePointerId == INVALID_POINTER) { 330 mActivePointerId = ev.getPointerId(i); 331 mLastTouchY = y; 332 mDragging = true; 333 } else if (isInChallengeView(x, y)) { 334 mBlockDrag = true; 335 } 336 } 337 break; 338 } 339 340 if (mBlockDrag) { 341 mActivePointerId = INVALID_POINTER; 342 mDragging = false; 343 } 344 345 return mDragging; 346 } 347 348 private void resetTouch() { 349 mVelocityTracker.recycle(); 350 mVelocityTracker = null; 351 mActivePointerId = INVALID_POINTER; 352 mDragging = mBlockDrag = false; 353 } 354 355 @Override 356 public boolean onTouchEvent(MotionEvent ev) { 357 if (mVelocityTracker == null) { 358 mVelocityTracker = VelocityTracker.obtain(); 359 } 360 mVelocityTracker.addMovement(ev); 361 362 final int action = ev.getActionMasked(); 363 switch (action) { 364 case MotionEvent.ACTION_CANCEL: 365 if (mDragging) { 366 showChallenge(0); 367 } 368 resetTouch(); 369 break; 370 371 case MotionEvent.ACTION_POINTER_UP: 372 if (mActivePointerId != ev.getPointerId(ev.getActionIndex())) { 373 break; 374 } 375 case MotionEvent.ACTION_UP: 376 if (mDragging) { 377 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); 378 showChallenge((int) mVelocityTracker.getYVelocity(mActivePointerId)); 379 } 380 resetTouch(); 381 break; 382 383 case MotionEvent.ACTION_MOVE: 384 if (!mDragging && !mBlockDrag) { 385 final int count = ev.getPointerCount(); 386 for (int i = 0; i < count; i++) { 387 final float x = ev.getX(i); 388 final float y = ev.getY(i); 389 390 if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mLastTouchY) || 391 (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING)) 392 && mActivePointerId == INVALID_POINTER) { 393 mActivePointerId = ev.getPointerId(i); 394 mDragging = true; 395 break; 396 } 397 } 398 } 399 // Not an else; this can be set above. 400 if (mDragging) { 401 // No-op if already in this state, but set it here in case we arrived 402 // at this point from either intercept or the above. 403 setScrollState(SCROLL_STATE_DRAGGING); 404 405 final int index = ev.findPointerIndex(mActivePointerId); 406 if (index < 0) { 407 // Oops, bogus state. We lost some touch events somewhere. 408 // Just drop it with no velocity and let things settle. 409 resetTouch(); 410 showChallenge(0); 411 return true; 412 } 413 final float y = Math.max(ev.getY(index), getChallengeOpenedTop()); 414 final float delta = y - mLastTouchY; 415 final int idelta = (int) delta; 416 // Don't lose the rounded component 417 mLastTouchY = y + delta - idelta; 418 419 moveChallengeBy(idelta); 420 } 421 break; 422 } 423 return true; 424 } 425 426 private boolean isInChallengeView(float x, float y) { 427 if (mChallengeView == null) return false; 428 429 return x >= mChallengeView.getLeft() && y >= mChallengeView.getTop() && 430 x < mChallengeView.getRight() && y < mChallengeView.getBottom(); 431 } 432 433 private boolean isInDragHandle(float x, float y) { 434 if (mChallengeView == null) return false; 435 436 return x >= 0 && y >= mChallengeView.getTop() && x < getWidth() && 437 y < mChallengeView.getTop() + mDragHandleSize; 438 } 439 440 private boolean crossedDragHandle(float x, float y, float lastY) { 441 final int challengeTop = mChallengeView.getTop(); 442 return x >= 0 && x < getWidth() && lastY < challengeTop && 443 y > challengeTop + mDragHandleSize; 444 } 445 446 @Override 447 protected void onMeasure(int widthSpec, int heightSpec) { 448 if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY || 449 MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) { 450 throw new IllegalArgumentException( 451 "SlidingChallengeLayout must be measured with an exact size"); 452 } 453 454 final int width = MeasureSpec.getSize(widthSpec); 455 final int height = MeasureSpec.getSize(heightSpec); 456 setMeasuredDimension(width, height); 457 458 // Find one and only one challenge view. 459 final View oldChallengeView = mChallengeView; 460 mChallengeView = null; 461 final int count = getChildCount(); 462 for (int i = 0; i < count; i++) { 463 final View child = getChildAt(i); 464 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 465 466 if (lp.isChallenge) { 467 if (mChallengeView != null) { 468 throw new IllegalStateException( 469 "There may only be one child with layout_isChallenge=\"true\""); 470 } 471 mChallengeView = child; 472 if (mChallengeView != oldChallengeView) { 473 mChallengeView.setVisibility(mChallengeShowing ? VISIBLE : INVISIBLE); 474 } 475 } 476 477 if (child.getVisibility() == GONE) continue; 478 479 measureChildWithMargins(child, widthSpec, 0, heightSpec, 0); 480 } 481 } 482 483 @Override 484 protected void onLayout(boolean changed, int l, int t, int r, int b) { 485 final int paddingLeft = getPaddingLeft(); 486 final int paddingTop = getPaddingTop(); 487 final int paddingRight = getPaddingRight(); 488 final int paddingBottom = getPaddingBottom(); 489 final int width = r - l; 490 final int height = b - t; 491 492 final int count = getChildCount(); 493 for (int i = 0; i < count; i++) { 494 final View child = getChildAt(i); 495 496 if (child.getVisibility() == GONE) continue; 497 498 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 499 500 if (lp.isChallenge) { 501 // Challenge views pin to the bottom, offset by a portion of their height, 502 // and center horizontally. 503 final int center = (paddingLeft + width - paddingRight) / 2; 504 final int childWidth = child.getMeasuredWidth(); 505 final int childHeight = child.getMeasuredHeight(); 506 final int left = center - childWidth / 2; 507 final int layoutBottom = height - paddingBottom - lp.bottomMargin; 508 // We use the top of the challenge view to position the handle, so 509 // we never want less than the handle size showing at the bottom. 510 final int bottom = layoutBottom + (int) ((childHeight - mDragHandleSize) 511 * (1 - mChallengeOffset)); 512 float offset = 1.f - (bottom - layoutBottom) / childHeight; 513 child.setAlpha(offset); 514 child.layout(left, bottom - childHeight, left + childWidth, bottom); 515 } else { 516 // Non-challenge views lay out from the upper left, layered. 517 child.layout(paddingLeft + lp.leftMargin, 518 paddingTop + lp.topMargin, 519 paddingLeft + child.getMeasuredWidth(), 520 paddingTop + child.getMeasuredHeight()); 521 } 522 } 523 524 if (!mHasLayout) { 525 // We want to trigger the initial listener updates outside of layout pass, 526 // in case the listeners trigger requestLayout(). 527 post(new Runnable() { 528 @Override 529 public void run() { 530 sendInitialListenerUpdates(); 531 } 532 }); 533 } 534 mHasLayout = true; 535 } 536 537 public void computeScroll() { 538 super.computeScroll(); 539 540 if (!mScroller.isFinished()) { 541 if (mChallengeView == null) { 542 // Can't scroll if the view is missing. 543 Log.e(TAG, "Challenge view missing in computeScroll"); 544 mScroller.abortAnimation(); 545 return; 546 } 547 548 mScroller.computeScrollOffset(); 549 moveChallengeTo(mScroller.getCurrY()); 550 551 if (mScroller.isFinished()) { 552 post(mEndScrollRunnable); 553 } 554 } 555 } 556 557 @Override 558 public void draw(Canvas c) { 559 super.draw(c); 560 if (mChallengeOffset < 1.f 561 && mChallengeView != null && mHandleDrawable != null && mShowHandle) { 562 final int top = mChallengeView.getTop(); 563 mHandleDrawable.setBounds(0, top, getWidth(), top + mDragHandleSize); 564 final float alpha = sHandleFadeInterpolator.getInterpolation(1 - mChallengeOffset); 565 mHandleDrawable.setAlpha((int) (alpha * 0xFF)); 566 mHandleDrawable.draw(c); 567 } 568 } 569 570 /** 571 * Move the bottom edge of mChallengeView to a new position and notify the listener 572 * if it represents a change in position. Changes made through this method will 573 * be stable across layout passes. If this method is called before first layout of 574 * this SlidingChallengeLayout it will have no effect. 575 * 576 * @param bottom New bottom edge in px in this SlidingChallengeLayout's coordinate system. 577 * @return true if the challenge view was moved 578 */ 579 private boolean moveChallengeTo(int bottom) { 580 if (mChallengeView == null || !mHasLayout) { 581 return false; 582 } 583 584 final int bottomMargin = ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin; 585 final int layoutBottom = getHeight() - getPaddingBottom() - bottomMargin; 586 final int challengeHeight = mChallengeView.getHeight(); 587 588 bottom = Math.max(layoutBottom, 589 Math.min(bottom, layoutBottom + challengeHeight - mDragHandleSize)); 590 591 float offset = 1.f - (float) (bottom - layoutBottom) / (challengeHeight - mDragHandleSize); 592 mChallengeOffset = offset; 593 if (offset > 0 && !mChallengeShowing) { 594 setChallengeShowing(true); 595 } 596 597 mChallengeView.layout(mChallengeView.getLeft(), 598 bottom - mChallengeView.getHeight(), mChallengeView.getRight(), bottom); 599 600 mChallengeView.setAlpha(offset); 601 if (mListener != null) { 602 mListener.onScrollPositionChanged(offset, mChallengeView.getTop()); 603 } 604 postInvalidateOnAnimation(); 605 return true; 606 } 607 608 private int getChallengeBottom() { 609 if (mChallengeView == null) return 0; 610 611 return mChallengeView.getBottom(); 612 } 613 614 private int getChallengeOpenedTop() { 615 final int paddedBottom = getHeight() - getPaddingBottom(); 616 if (mChallengeView == null) return paddedBottom; 617 618 final int bottomMargin = ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin; 619 final int layoutBottom = paddedBottom - bottomMargin; 620 621 return layoutBottom - mChallengeView.getHeight(); 622 } 623 624 private void moveChallengeBy(int delta) { 625 moveChallengeTo(getChallengeBottom() + delta); 626 } 627 628 /** 629 * Show or hide the challenge view, animating it if necessary. 630 * @param show true to show, false to hide 631 */ 632 public void showChallenge(boolean show) { 633 showChallenge(show, 0); 634 } 635 636 private void showChallenge(int velocity) { 637 boolean show = false; 638 if (Math.abs(velocity) > mMinVelocity) { 639 show = velocity < 0; 640 } else { 641 show = mChallengeOffset >= 0.5f; 642 } 643 showChallenge(show, velocity); 644 } 645 646 private void showChallenge(boolean show, int velocity) { 647 if (mChallengeView == null) { 648 setChallengeShowing(false); 649 return; 650 } 651 652 if (mHasLayout) { 653 final int bottomMargin = ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin; 654 final int layoutBottom = getHeight() - getPaddingBottom() - bottomMargin; 655 animateChallengeTo(show ? layoutBottom : 656 layoutBottom + mChallengeView.getHeight() - mDragHandleSize, velocity); 657 } 658 } 659 660 @Override 661 public void showBouncer() { 662 // TODO Block access to other views 663 showChallenge(true); 664 } 665 666 public void showHandle(boolean show) { 667 mShowHandle = show; 668 invalidate(); 669 } 670 671 @Override 672 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 673 return new LayoutParams(getContext(), attrs); 674 } 675 676 @Override 677 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 678 return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) : 679 p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : 680 new LayoutParams(p); 681 } 682 683 @Override 684 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 685 return new LayoutParams(); 686 } 687 688 @Override 689 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 690 return p instanceof LayoutParams; 691 } 692 693 public static class LayoutParams extends MarginLayoutParams { 694 public boolean isChallenge = false; 695 696 public LayoutParams() { 697 this(MATCH_PARENT, WRAP_CONTENT); 698 } 699 700 public LayoutParams(int width, int height) { 701 super(width, height); 702 } 703 704 public LayoutParams(android.view.ViewGroup.LayoutParams source) { 705 super(source); 706 } 707 708 public LayoutParams(MarginLayoutParams source) { 709 super(source); 710 } 711 712 public LayoutParams(LayoutParams source) { 713 super(source); 714 715 isChallenge = source.isChallenge; 716 } 717 718 public LayoutParams(Context c, AttributeSet attrs) { 719 super(c, attrs); 720 721 final TypedArray a = c.obtainStyledAttributes(attrs, 722 R.styleable.SlidingChallengeLayout_Layout); 723 isChallenge = a.getBoolean(R.styleable.SlidingChallengeLayout_Layout_layout_isChallenge, 724 false); 725 a.recycle(); 726 } 727 } 728} 729