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