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