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