1/* 2* Copyright 2013 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.example.android.batchstepsensor.cardstream; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.app.Activity; 23import android.graphics.Color; 24import android.view.LayoutInflater; 25import android.view.View; 26import android.view.ViewGroup; 27import android.widget.Button; 28import android.widget.ProgressBar; 29import android.widget.TextView; 30 31import com.example.android.batchstepsensor.R; 32 33import java.util.ArrayList; 34 35/** 36 * A Card contains a description and has a visual state. Optionally a card also contains a title, 37 * progress indicator and zero or more actions. It is constructed through the {@link Builder}. 38 */ 39public class Card { 40 41 public static final int ACTION_POSITIVE = 1; 42 public static final int ACTION_NEGATIVE = 2; 43 public static final int ACTION_NEUTRAL = 3; 44 45 public static final int PROGRESS_TYPE_NO_PROGRESS = 0; 46 public static final int PROGRESS_TYPE_NORMAL = 1; 47 public static final int PROGRESS_TYPE_INDETERMINATE = 2; 48 public static final int PROGRESS_TYPE_LABEL = 3; 49 50 private OnCardClickListener mClickListener; 51 52 53 // The card model contains a reference to its desired layout (for extensibility), title, 54 // description, zero to many action buttons, and zero or 1 progress indicators. 55 private int mLayoutId = R.layout.card; 56 57 /** 58 * Tag that uniquely identifies this card. 59 */ 60 private String mTag = null; 61 62 private String mTitle = null; 63 private String mDescription = null; 64 65 private View mCardView = null; 66 private View mOverlayView = null; 67 private TextView mTitleView = null; 68 private TextView mDescView = null; 69 private View mActionAreaView = null; 70 71 private Animator mOngoingAnimator = null; 72 73 /** 74 * Visual state, either {@link #CARD_STATE_NORMAL}, {@link #CARD_STATE_FOCUSED} or 75 * {@link #CARD_STATE_INACTIVE}. 76 */ 77 private int mCardState = CARD_STATE_NORMAL; 78 public static final int CARD_STATE_NORMAL = 1; 79 public static final int CARD_STATE_FOCUSED = 2; 80 public static final int CARD_STATE_INACTIVE = 3; 81 82 /** 83 * Represent actions that can be taken from the card. Stylistically the developer can 84 * designate the action as positive, negative (ok/cancel, for instance), or neutral. 85 * This "type" can be used as a UI hint. 86 * @see com.example.android.sensors.batchstepsensor.Card.CardAction 87 */ 88 private ArrayList<CardAction> mCardActions = new ArrayList<CardAction>(); 89 90 /** 91 * Some cards will have a sense of "progress" which should be associated with, but separated 92 * from its "parent" card. To push for simplicity in samples, Cards are designed to have 93 * a maximum of one progress indicator per Card. 94 */ 95 private CardProgress mCardProgress = null; 96 97 public Card() { 98 } 99 100 public String getTag() { 101 return mTag; 102 } 103 104 public View getView() { 105 return mCardView; 106 } 107 108 109 public Card setDescription(String desc) { 110 if (mDescView != null) { 111 mDescription = desc; 112 mDescView.setText(desc); 113 } 114 return this; 115 } 116 117 public Card setTitle(String title) { 118 if (mTitleView != null) { 119 mTitle = title; 120 mTitleView.setText(title); 121 } 122 return this; 123 } 124 125 126 /** 127 * Return the UI state, either {@link #CARD_STATE_NORMAL}, {@link #CARD_STATE_FOCUSED} 128 * or {@link #CARD_STATE_INACTIVE}. 129 */ 130 public int getState() { 131 return mCardState; 132 } 133 134 /** 135 * Set the UI state. The parameter describes the state and must be either 136 * {@link #CARD_STATE_NORMAL}, {@link #CARD_STATE_FOCUSED} or {@link #CARD_STATE_INACTIVE}. 137 * Note: This method must be called from the UI Thread. 138 * @param state 139 * @return The card itself, allows for chaining of calls 140 */ 141 public Card setState(int state) { 142 mCardState = state; 143 if (null != mOverlayView) { 144 if (null != mOngoingAnimator) { 145 mOngoingAnimator.end(); 146 mOngoingAnimator = null; 147 } 148 switch (state) { 149 case CARD_STATE_NORMAL: { 150 mOverlayView.setVisibility(View.GONE); 151 mOverlayView.setAlpha(1.f); 152 break; 153 } 154 case CARD_STATE_FOCUSED: { 155 mOverlayView.setVisibility(View.VISIBLE); 156 mOverlayView.setBackgroundResource(R.drawable.card_overlay_focused); 157 ObjectAnimator animator = ObjectAnimator.ofFloat(mOverlayView, "alpha", 0.f); 158 animator.setRepeatMode(ObjectAnimator.REVERSE); 159 animator.setRepeatCount(ObjectAnimator.INFINITE); 160 animator.setDuration(1000); 161 animator.start(); 162 mOngoingAnimator = animator; 163 break; 164 } 165 case CARD_STATE_INACTIVE: { 166 mOverlayView.setVisibility(View.VISIBLE); 167 mOverlayView.setAlpha(1.f); 168 mOverlayView.setBackgroundColor(Color.argb(0xaa, 0xcc, 0xcc, 0xcc)); 169 break; 170 } 171 } 172 } 173 return this; 174 } 175 176 /** 177 * Set the type of progress indicator. 178 * The progress type can only be changed if the Card was initially build with a progress 179 * indicator. 180 * See {@link Builder#setProgressType(int)}. 181 * Must be a value of either {@link #PROGRESS_TYPE_NORMAL}, 182 * {@link #PROGRESS_TYPE_INDETERMINATE}, {@link #PROGRESS_TYPE_LABEL} or 183 * {@link #PROGRESS_TYPE_NO_PROGRESS}. 184 * @param progressType 185 * @return The card itself, allows for chaining of calls 186 */ 187 public Card setProgressType(int progressType) { 188 if (mCardProgress == null) { 189 mCardProgress = new CardProgress(); 190 } 191 mCardProgress.setProgressType(progressType); 192 return this; 193 } 194 195 /** 196 * Return the progress indicator type. A value of either {@link #PROGRESS_TYPE_NORMAL}, 197 * {@link #PROGRESS_TYPE_INDETERMINATE}, {@link #PROGRESS_TYPE_LABEL}. Otherwise if no progress 198 * indicator is enabled, {@link #PROGRESS_TYPE_NO_PROGRESS} is returned. 199 * @return 200 */ 201 public int getProgressType() { 202 if (mCardProgress == null) { 203 return PROGRESS_TYPE_NO_PROGRESS; 204 } 205 return mCardProgress.progressType; 206 } 207 208 /** 209 * Set the progress to the specified value. Only applicable if the card has a 210 * {@link #PROGRESS_TYPE_NORMAL} progress type. 211 * @param progress 212 * @return 213 * @see #setMaxProgress(int) 214 */ 215 public Card setProgress(int progress) { 216 if (mCardProgress != null) { 217 mCardProgress.setProgress(progress); 218 } 219 return this; 220 } 221 222 /** 223 * Set the range of the progress to 0...max. Only applicable if the card has a 224 * {@link #PROGRESS_TYPE_NORMAL} progress type. 225 * @return 226 */ 227 public Card setMaxProgress(int max){ 228 if (mCardProgress != null) { 229 mCardProgress.setMax(max); 230 } 231 return this; 232 } 233 234 /** 235 * Set the label text for the progress if the card has a progress type of 236 * {@link #PROGRESS_TYPE_NORMAL}, {@link #PROGRESS_TYPE_INDETERMINATE} or 237 * {@link #PROGRESS_TYPE_LABEL} 238 * @param text 239 * @return 240 */ 241 public Card setProgressLabel(String text) { 242 if (mCardProgress != null) { 243 mCardProgress.setProgressLabel(text); 244 } 245 return this; 246 } 247 248 /** 249 * Toggle the visibility of the progress section of the card. Only applicable if 250 * the card has a progress type of 251 * {@link #PROGRESS_TYPE_NORMAL}, {@link #PROGRESS_TYPE_INDETERMINATE} or 252 * {@link #PROGRESS_TYPE_LABEL}. 253 * @param isVisible 254 * @return 255 */ 256 public Card setProgressVisibility(boolean isVisible) { 257 if (mCardProgress.progressView == null) { 258 return this; // Card does not have progress 259 } 260 mCardProgress.progressView.setVisibility(isVisible ? View.VISIBLE : View.GONE); 261 262 return this; 263 } 264 265 /** 266 * Adds an action to this card during build time. 267 * 268 * @param label 269 * @param id 270 * @param type 271 */ 272 private void addAction(String label, int id, int type) { 273 CardAction cardAction = new CardAction(); 274 cardAction.label = label; 275 cardAction.id = id; 276 cardAction.type = type; 277 mCardActions.add(cardAction); 278 } 279 280 /** 281 * Toggles the visibility of a card action. 282 * @param actionId 283 * @param isVisible 284 * @return 285 */ 286 public Card setActionVisibility(int actionId, boolean isVisible) { 287 int visibilityFlag = isVisible ? View.VISIBLE : View.GONE; 288 for (CardAction action : mCardActions) { 289 if (action.id == actionId && action.actionView != null) { 290 action.actionView.setVisibility(visibilityFlag); 291 } 292 } 293 return this; 294 } 295 296 /** 297 * Toggles visibility of the action area of this Card through an animation. 298 * @param isVisible 299 * @return 300 */ 301 public Card setActionAreaVisibility(boolean isVisible) { 302 if (mActionAreaView == null) { 303 return this; // Card does not have an action area 304 } 305 306 if (isVisible) { 307 // Show the action area 308 mActionAreaView.setVisibility(View.VISIBLE); 309 mActionAreaView.setPivotY(0.f); 310 mActionAreaView.setPivotX(mCardView.getWidth() / 2.f); 311 mActionAreaView.setAlpha(0.5f); 312 mActionAreaView.setRotationX(-90.f); 313 mActionAreaView.animate().rotationX(0.f).alpha(1.f).setDuration(400); 314 } else { 315 // Hide the action area 316 mActionAreaView.setPivotY(0.f); 317 mActionAreaView.setPivotX(mCardView.getWidth() / 2.f); 318 mActionAreaView.animate().rotationX(-90.f).alpha(0.f).setDuration(400).setListener( 319 new AnimatorListenerAdapter() { 320 @Override 321 public void onAnimationEnd(Animator animation) { 322 mActionAreaView.setVisibility(View.GONE); 323 } 324 }); 325 } 326 return this; 327 } 328 329 330 /** 331 * Creates a shallow clone of the card. Shallow means all values are present, but no views. 332 * This is useful for saving/restoring in the case of configuration changes, like screen 333 * rotation. 334 * 335 * @return A shallow clone of the card instance 336 */ 337 public Card createShallowClone() { 338 Card cloneCard = new Card(); 339 340 // Outer card values 341 cloneCard.mTitle = mTitle; 342 cloneCard.mDescription = mDescription; 343 cloneCard.mTag = mTag; 344 cloneCard.mLayoutId = mLayoutId; 345 cloneCard.mCardState = mCardState; 346 347 // Progress 348 if (mCardProgress != null) { 349 cloneCard.mCardProgress = mCardProgress.createShallowClone(); 350 } 351 352 // Actions 353 for (CardAction action : mCardActions) { 354 cloneCard.mCardActions.add(action.createShallowClone()); 355 } 356 357 return cloneCard; 358 } 359 360 361 /** 362 * Prepare the card to be stored for configuration change. 363 */ 364 public void prepareForConfigurationChange() { 365 // Null out views. 366 mCardView = null; 367 for (CardAction action : mCardActions) { 368 action.actionView = null; 369 } 370 mCardProgress.progressView = null; 371 } 372 373 /** 374 * Creates a new {@link #Card}. 375 */ 376 public static class Builder { 377 private Card mCard; 378 379 /** 380 * Instantiate the builder with data from a shallow clone. 381 * @param listener 382 * @param card 383 * @see Card#createShallowClone() 384 */ 385 protected Builder(OnCardClickListener listener, Card card) { 386 mCard = card; 387 mCard.mClickListener = listener; 388 } 389 390 /** 391 * Instantiate the builder with the tag of the card. 392 * @param listener 393 * @param tag 394 */ 395 public Builder(OnCardClickListener listener, String tag) { 396 mCard = new Card(); 397 mCard.mTag = tag; 398 mCard.mClickListener = listener; 399 } 400 401 public Builder setTitle(String title) { 402 mCard.mTitle = title; 403 return this; 404 } 405 406 public Builder setDescription(String desc) { 407 mCard.mDescription = desc; 408 return this; 409 } 410 411 /** 412 * Add an action. 413 * The type describes how this action will be displayed. Accepted values are 414 * {@link #ACTION_NEUTRAL}, {@link #ACTION_POSITIVE} or {@link #ACTION_NEGATIVE}. 415 * 416 * @param label The text to display for this action 417 * @param id Identifier for this action, supplied in the click listener 418 * @param type UI style of action 419 * @return 420 */ 421 public Builder addAction(String label, int id, int type) { 422 mCard.addAction(label, id, type); 423 return this; 424 } 425 426 /** 427 * Override the default layout. 428 * The referenced layout file has to contain the same identifiers as defined in the default 429 * layout configuration. 430 * @param layout 431 * @return 432 * @see R.layout.card 433 */ 434 public Builder setLayout(int layout) { 435 mCard.mLayoutId = layout; 436 return this; 437 } 438 439 /** 440 * Set the type of progress bar to display. 441 * Accepted values are: 442 * <ul> 443 * <li>{@link #PROGRESS_TYPE_NO_PROGRESS} disables the progress indicator</li> 444 * <li>{@link #PROGRESS_TYPE_NORMAL} 445 * displays a standard, linear progress indicator.</li> 446 * <li>{@link #PROGRESS_TYPE_INDETERMINATE} displays an indeterminate (infite) progress 447 * indicator.</li> 448 * <li>{@link #PROGRESS_TYPE_LABEL} only displays a label text in the progress area 449 * of the card.</li> 450 * </ul> 451 * 452 * @param progressType 453 * @return 454 */ 455 public Builder setProgressType(int progressType) { 456 mCard.setProgressType(progressType); 457 return this; 458 } 459 460 public Builder setProgressLabel(String label) { 461 // ensure the progress layout has been initialized, use 'no progress' by default 462 if (mCard.mCardProgress == null) { 463 mCard.setProgressType(PROGRESS_TYPE_NO_PROGRESS); 464 } 465 mCard.mCardProgress.label = label; 466 return this; 467 } 468 469 public Builder setProgressMaxValue(int maxValue) { 470 // ensure the progress layout has been initialized, use 'no progress' by default 471 if (mCard.mCardProgress == null) { 472 mCard.setProgressType(PROGRESS_TYPE_NO_PROGRESS); 473 } 474 mCard.mCardProgress.maxValue = maxValue; 475 return this; 476 } 477 478 public Builder setStatus(int status) { 479 mCard.setState(status); 480 return this; 481 } 482 483 public Card build(Activity activity) { 484 LayoutInflater inflater = activity.getLayoutInflater(); 485 // Inflating the card. 486 ViewGroup cardView = (ViewGroup) inflater.inflate(mCard.mLayoutId, 487 (ViewGroup) activity.findViewById(R.id.card_stream), false); 488 489 // Check that the layout contains a TextView with the card_title id 490 View viewTitle = cardView.findViewById(R.id.card_title); 491 if (mCard.mTitle != null && viewTitle != null) { 492 mCard.mTitleView = (TextView) viewTitle; 493 mCard.mTitleView.setText(mCard.mTitle); 494 } else if (viewTitle != null) { 495 viewTitle.setVisibility(View.GONE); 496 } 497 498 // Check that the layout contains a TextView with the card_content id 499 View viewDesc = cardView.findViewById(R.id.card_content); 500 if (mCard.mDescription != null && viewDesc != null) { 501 mCard.mDescView = (TextView) viewDesc; 502 mCard.mDescView.setText(mCard.mDescription); 503 } else if (viewDesc != null) { 504 cardView.findViewById(R.id.card_content).setVisibility(View.GONE); 505 } 506 507 508 ViewGroup actionArea = (ViewGroup) cardView.findViewById(R.id.card_actionarea); 509 510 // Inflate Progress 511 initializeProgressView(inflater, actionArea); 512 513 // Inflate all action views. 514 initializeActionViews(inflater, cardView, actionArea); 515 516 mCard.mCardView = cardView; 517 mCard.mOverlayView = cardView.findViewById(R.id.card_overlay); 518 519 return mCard; 520 } 521 522 /** 523 * Initialize data from the given card. 524 * @param card 525 * @return 526 * @see Card#createShallowClone() 527 */ 528 public Builder cloneFromCard(Card card) { 529 mCard = card.createShallowClone(); 530 return this; 531 } 532 533 /** 534 * Build the action views by inflating the appropriate layouts and setting the text and 535 * values. 536 * @param inflater 537 * @param cardView 538 * @param actionArea 539 */ 540 private void initializeActionViews(LayoutInflater inflater, ViewGroup cardView, 541 ViewGroup actionArea) { 542 if (!mCard.mCardActions.isEmpty()) { 543 // Set action area to visible only when actions are visible 544 actionArea.setVisibility(View.VISIBLE); 545 mCard.mActionAreaView = actionArea; 546 } 547 548 // Inflate all card actions 549 for (final CardAction action : mCard.mCardActions) { 550 551 int useActionLayout = 0; 552 switch (action.type) { 553 case Card.ACTION_POSITIVE: 554 useActionLayout = R.layout.card_button_positive; 555 break; 556 case Card.ACTION_NEGATIVE: 557 useActionLayout = R.layout.card_button_negative; 558 break; 559 case Card.ACTION_NEUTRAL: 560 default: 561 useActionLayout = R.layout.card_button_neutral; 562 break; 563 } 564 565 action.actionView = inflater.inflate(useActionLayout, actionArea, false); 566 Button actionButton = (Button) action.actionView.findViewById(R.id.card_button); 567 568 actionButton.setText(action.label); 569 actionButton.setOnClickListener(new View.OnClickListener() { 570 @Override 571 public void onClick(View v) { 572 mCard.mClickListener.onCardClick(action.id, mCard.mTag); 573 } 574 }); 575 actionArea.addView(action.actionView); 576 } 577 } 578 579 /** 580 * Build the progress view into the given ViewGroup. 581 * 582 * @param inflater 583 * @param actionArea 584 */ 585 private void initializeProgressView(LayoutInflater inflater, ViewGroup actionArea) { 586 587 // Only inflate progress layout if a progress type other than NO_PROGRESS was set. 588 if (mCard.mCardProgress != null) { 589 //Setup progress card. 590 View progressView = inflater.inflate(R.layout.card_progress, actionArea, false); 591 ProgressBar progressBar = 592 (ProgressBar) progressView.findViewById(R.id.card_progress); 593 ((TextView) progressView.findViewById(R.id.card_progress_text)) 594 .setText(mCard.mCardProgress.label); 595 progressBar.setMax(mCard.mCardProgress.maxValue); 596 progressBar.setProgress(0); 597 mCard.mCardProgress.progressView = progressView; 598 mCard.mCardProgress.setProgressType(mCard.getProgressType()); 599 actionArea.addView(progressView); 600 } 601 } 602 } 603 604 /** 605 * Represents a clickable action, accessible from the bottom of the card. 606 * Fields include the label, an ID to specify the action that was performed in the callback, 607 * an action type (positive, negative, neutral), and the callback. 608 */ 609 public class CardAction { 610 611 public String label; 612 public int id; 613 public int type; 614 public View actionView; 615 616 public CardAction createShallowClone() { 617 CardAction actionClone = new CardAction(); 618 actionClone.label = label; 619 actionClone.id = id; 620 actionClone.type = type; 621 return actionClone; 622 // Not the view. Never the view (don't want to hold view references for 623 // onConfigurationChange. 624 } 625 626 } 627 628 /** 629 * Describes the progress of a {@link Card}. 630 * Three types of progress are supported: 631 * <ul><li>{@link Card#PROGRESS_TYPE_NORMAL: Standard progress bar with label text</li> 632 * <li>{@link Card#PROGRESS_TYPE_INDETERMINATE}: Indeterminate progress bar with label txt</li> 633 * <li>{@link Card#PROGRESS_TYPE_LABEL}: Label only, no progresss bar</li> 634 * </ul> 635 */ 636 public class CardProgress { 637 private int progressType = Card.PROGRESS_TYPE_NO_PROGRESS; 638 private String label = ""; 639 private int currProgress = 0; 640 private int maxValue = 100; 641 642 public View progressView = null; 643 private ProgressBar progressBar = null; 644 private TextView progressLabel = null; 645 646 public CardProgress createShallowClone() { 647 CardProgress progressClone = new CardProgress(); 648 progressClone.label = label; 649 progressClone.currProgress = currProgress; 650 progressClone.maxValue = maxValue; 651 progressClone.progressType = progressType; 652 return progressClone; 653 } 654 655 /** 656 * Set the progress. Only useful for the type {@link #PROGRESS_TYPE_NORMAL}. 657 * @param progress 658 * @see android.widget.ProgressBar#setProgress(int) 659 */ 660 public void setProgress(int progress) { 661 currProgress = progress; 662 final ProgressBar bar = getProgressBar(); 663 if (bar != null) { 664 bar.setProgress(currProgress); 665 bar.invalidate(); 666 } 667 } 668 669 /** 670 * Set the range of the progress to 0...max. 671 * Only useful for the type {@link #PROGRESS_TYPE_NORMAL}. 672 * @param max 673 * @see android.widget.ProgressBar#setMax(int) 674 */ 675 public void setMax(int max) { 676 maxValue = max; 677 final ProgressBar bar = getProgressBar(); 678 if (bar != null) { 679 bar.setMax(maxValue); 680 } 681 } 682 683 /** 684 * Set the label text that appears near the progress indicator. 685 * @param text 686 */ 687 public void setProgressLabel(String text) { 688 label = text; 689 final TextView labelView = getProgressLabel(); 690 if (labelView != null) { 691 labelView.setText(text); 692 } 693 } 694 695 /** 696 * Set how progress is displayed. The parameter must be one of three supported types: 697 * <ul><li>{@link Card#PROGRESS_TYPE_NORMAL: Standard progress bar with label text</li> 698 * <li>{@link Card#PROGRESS_TYPE_INDETERMINATE}: 699 * Indeterminate progress bar with label txt</li> 700 * <li>{@link Card#PROGRESS_TYPE_LABEL}: Label only, no progresss bar</li> 701 * @param type 702 */ 703 public void setProgressType(int type) { 704 progressType = type; 705 if (progressView != null) { 706 switch (type) { 707 case PROGRESS_TYPE_NO_PROGRESS: { 708 progressView.setVisibility(View.GONE); 709 break; 710 } 711 case PROGRESS_TYPE_NORMAL: { 712 progressView.setVisibility(View.VISIBLE); 713 getProgressBar().setIndeterminate(false); 714 break; 715 } 716 case PROGRESS_TYPE_INDETERMINATE: { 717 progressView.setVisibility(View.VISIBLE); 718 getProgressBar().setIndeterminate(true); 719 break; 720 } 721 } 722 } 723 } 724 725 private TextView getProgressLabel() { 726 if (progressLabel != null) { 727 return progressLabel; 728 } else if (progressView != null) { 729 progressLabel = (TextView) progressView.findViewById(R.id.card_progress_text); 730 return progressLabel; 731 } else { 732 return null; 733 } 734 } 735 736 private ProgressBar getProgressBar() { 737 if (progressBar != null) { 738 return progressBar; 739 } else if (progressView != null) { 740 progressBar = (ProgressBar) progressView.findViewById(R.id.card_progress); 741 return progressBar; 742 } else { 743 return null; 744 } 745 } 746 747 } 748} 749 750