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