/* * Copyright 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.batchstepsensor.cardstream; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.app.Activity; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import com.example.android.batchstepsensor.R; import java.util.ArrayList; /** * A Card contains a description and has a visual state. Optionally a card also contains a title, * progress indicator and zero or more actions. It is constructed through the {@link Builder}. */ public class Card { public static final int ACTION_POSITIVE = 1; public static final int ACTION_NEGATIVE = 2; public static final int ACTION_NEUTRAL = 3; public static final int PROGRESS_TYPE_NO_PROGRESS = 0; public static final int PROGRESS_TYPE_NORMAL = 1; public static final int PROGRESS_TYPE_INDETERMINATE = 2; public static final int PROGRESS_TYPE_LABEL = 3; private OnCardClickListener mClickListener; // The card model contains a reference to its desired layout (for extensibility), title, // description, zero to many action buttons, and zero or 1 progress indicators. private int mLayoutId = R.layout.card; /** * Tag that uniquely identifies this card. */ private String mTag = null; private String mTitle = null; private String mDescription = null; private View mCardView = null; private View mOverlayView = null; private TextView mTitleView = null; private TextView mDescView = null; private View mActionAreaView = null; private Animator mOngoingAnimator = null; /** * Visual state, either {@link #CARD_STATE_NORMAL}, {@link #CARD_STATE_FOCUSED} or * {@link #CARD_STATE_INACTIVE}. */ private int mCardState = CARD_STATE_NORMAL; public static final int CARD_STATE_NORMAL = 1; public static final int CARD_STATE_FOCUSED = 2; public static final int CARD_STATE_INACTIVE = 3; /** * Represent actions that can be taken from the card. Stylistically the developer can * designate the action as positive, negative (ok/cancel, for instance), or neutral. * This "type" can be used as a UI hint. * @see com.example.android.sensors.batchstepsensor.Card.CardAction */ private ArrayList mCardActions = new ArrayList(); /** * Some cards will have a sense of "progress" which should be associated with, but separated * from its "parent" card. To push for simplicity in samples, Cards are designed to have * a maximum of one progress indicator per Card. */ private CardProgress mCardProgress = null; public Card() { } public String getTag() { return mTag; } public View getView() { return mCardView; } public Card setDescription(String desc) { if (mDescView != null) { mDescription = desc; mDescView.setText(desc); } return this; } public Card setTitle(String title) { if (mTitleView != null) { mTitle = title; mTitleView.setText(title); } return this; } /** * Return the UI state, either {@link #CARD_STATE_NORMAL}, {@link #CARD_STATE_FOCUSED} * or {@link #CARD_STATE_INACTIVE}. */ public int getState() { return mCardState; } /** * Set the UI state. The parameter describes the state and must be either * {@link #CARD_STATE_NORMAL}, {@link #CARD_STATE_FOCUSED} or {@link #CARD_STATE_INACTIVE}. * Note: This method must be called from the UI Thread. * @param state * @return The card itself, allows for chaining of calls */ public Card setState(int state) { mCardState = state; if (null != mOverlayView) { if (null != mOngoingAnimator) { mOngoingAnimator.end(); mOngoingAnimator = null; } switch (state) { case CARD_STATE_NORMAL: { mOverlayView.setVisibility(View.GONE); mOverlayView.setAlpha(1.f); break; } case CARD_STATE_FOCUSED: { mOverlayView.setVisibility(View.VISIBLE); mOverlayView.setBackgroundResource(R.drawable.card_overlay_focused); ObjectAnimator animator = ObjectAnimator.ofFloat(mOverlayView, "alpha", 0.f); animator.setRepeatMode(ObjectAnimator.REVERSE); animator.setRepeatCount(ObjectAnimator.INFINITE); animator.setDuration(1000); animator.start(); mOngoingAnimator = animator; break; } case CARD_STATE_INACTIVE: { mOverlayView.setVisibility(View.VISIBLE); mOverlayView.setAlpha(1.f); mOverlayView.setBackgroundColor(Color.argb(0xaa, 0xcc, 0xcc, 0xcc)); break; } } } return this; } /** * Set the type of progress indicator. * The progress type can only be changed if the Card was initially build with a progress * indicator. * See {@link Builder#setProgressType(int)}. * Must be a value of either {@link #PROGRESS_TYPE_NORMAL}, * {@link #PROGRESS_TYPE_INDETERMINATE}, {@link #PROGRESS_TYPE_LABEL} or * {@link #PROGRESS_TYPE_NO_PROGRESS}. * @param progressType * @return The card itself, allows for chaining of calls */ public Card setProgressType(int progressType) { if (mCardProgress == null) { mCardProgress = new CardProgress(); } mCardProgress.setProgressType(progressType); return this; } /** * Return the progress indicator type. A value of either {@link #PROGRESS_TYPE_NORMAL}, * {@link #PROGRESS_TYPE_INDETERMINATE}, {@link #PROGRESS_TYPE_LABEL}. Otherwise if no progress * indicator is enabled, {@link #PROGRESS_TYPE_NO_PROGRESS} is returned. * @return */ public int getProgressType() { if (mCardProgress == null) { return PROGRESS_TYPE_NO_PROGRESS; } return mCardProgress.progressType; } /** * Set the progress to the specified value. Only applicable if the card has a * {@link #PROGRESS_TYPE_NORMAL} progress type. * @param progress * @return * @see #setMaxProgress(int) */ public Card setProgress(int progress) { if (mCardProgress != null) { mCardProgress.setProgress(progress); } return this; } /** * Set the range of the progress to 0...max. Only applicable if the card has a * {@link #PROGRESS_TYPE_NORMAL} progress type. * @return */ public Card setMaxProgress(int max){ if (mCardProgress != null) { mCardProgress.setMax(max); } return this; } /** * Set the label text for the progress if the card has a progress type of * {@link #PROGRESS_TYPE_NORMAL}, {@link #PROGRESS_TYPE_INDETERMINATE} or * {@link #PROGRESS_TYPE_LABEL} * @param text * @return */ public Card setProgressLabel(String text) { if (mCardProgress != null) { mCardProgress.setProgressLabel(text); } return this; } /** * Toggle the visibility of the progress section of the card. Only applicable if * the card has a progress type of * {@link #PROGRESS_TYPE_NORMAL}, {@link #PROGRESS_TYPE_INDETERMINATE} or * {@link #PROGRESS_TYPE_LABEL}. * @param isVisible * @return */ public Card setProgressVisibility(boolean isVisible) { if (mCardProgress.progressView == null) { return this; // Card does not have progress } mCardProgress.progressView.setVisibility(isVisible ? View.VISIBLE : View.GONE); return this; } /** * Adds an action to this card during build time. * * @param label * @param id * @param type */ private void addAction(String label, int id, int type) { CardAction cardAction = new CardAction(); cardAction.label = label; cardAction.id = id; cardAction.type = type; mCardActions.add(cardAction); } /** * Toggles the visibility of a card action. * @param actionId * @param isVisible * @return */ public Card setActionVisibility(int actionId, boolean isVisible) { int visibilityFlag = isVisible ? View.VISIBLE : View.GONE; for (CardAction action : mCardActions) { if (action.id == actionId && action.actionView != null) { action.actionView.setVisibility(visibilityFlag); } } return this; } /** * Toggles visibility of the action area of this Card through an animation. * @param isVisible * @return */ public Card setActionAreaVisibility(boolean isVisible) { if (mActionAreaView == null) { return this; // Card does not have an action area } if (isVisible) { // Show the action area mActionAreaView.setVisibility(View.VISIBLE); mActionAreaView.setPivotY(0.f); mActionAreaView.setPivotX(mCardView.getWidth() / 2.f); mActionAreaView.setAlpha(0.5f); mActionAreaView.setRotationX(-90.f); mActionAreaView.animate().rotationX(0.f).alpha(1.f).setDuration(400); } else { // Hide the action area mActionAreaView.setPivotY(0.f); mActionAreaView.setPivotX(mCardView.getWidth() / 2.f); mActionAreaView.animate().rotationX(-90.f).alpha(0.f).setDuration(400).setListener( new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mActionAreaView.setVisibility(View.GONE); } }); } return this; } /** * Creates a shallow clone of the card. Shallow means all values are present, but no views. * This is useful for saving/restoring in the case of configuration changes, like screen * rotation. * * @return A shallow clone of the card instance */ public Card createShallowClone() { Card cloneCard = new Card(); // Outer card values cloneCard.mTitle = mTitle; cloneCard.mDescription = mDescription; cloneCard.mTag = mTag; cloneCard.mLayoutId = mLayoutId; cloneCard.mCardState = mCardState; // Progress if (mCardProgress != null) { cloneCard.mCardProgress = mCardProgress.createShallowClone(); } // Actions for (CardAction action : mCardActions) { cloneCard.mCardActions.add(action.createShallowClone()); } return cloneCard; } /** * Prepare the card to be stored for configuration change. */ public void prepareForConfigurationChange() { // Null out views. mCardView = null; for (CardAction action : mCardActions) { action.actionView = null; } mCardProgress.progressView = null; } /** * Creates a new {@link #Card}. */ public static class Builder { private Card mCard; /** * Instantiate the builder with data from a shallow clone. * @param listener * @param card * @see Card#createShallowClone() */ protected Builder(OnCardClickListener listener, Card card) { mCard = card; mCard.mClickListener = listener; } /** * Instantiate the builder with the tag of the card. * @param listener * @param tag */ public Builder(OnCardClickListener listener, String tag) { mCard = new Card(); mCard.mTag = tag; mCard.mClickListener = listener; } public Builder setTitle(String title) { mCard.mTitle = title; return this; } public Builder setDescription(String desc) { mCard.mDescription = desc; return this; } /** * Add an action. * The type describes how this action will be displayed. Accepted values are * {@link #ACTION_NEUTRAL}, {@link #ACTION_POSITIVE} or {@link #ACTION_NEGATIVE}. * * @param label The text to display for this action * @param id Identifier for this action, supplied in the click listener * @param type UI style of action * @return */ public Builder addAction(String label, int id, int type) { mCard.addAction(label, id, type); return this; } /** * Override the default layout. * The referenced layout file has to contain the same identifiers as defined in the default * layout configuration. * @param layout * @return * @see R.layout.card */ public Builder setLayout(int layout) { mCard.mLayoutId = layout; return this; } /** * Set the type of progress bar to display. * Accepted values are: * * * @param progressType * @return */ public Builder setProgressType(int progressType) { mCard.setProgressType(progressType); return this; } public Builder setProgressLabel(String label) { // ensure the progress layout has been initialized, use 'no progress' by default if (mCard.mCardProgress == null) { mCard.setProgressType(PROGRESS_TYPE_NO_PROGRESS); } mCard.mCardProgress.label = label; return this; } public Builder setProgressMaxValue(int maxValue) { // ensure the progress layout has been initialized, use 'no progress' by default if (mCard.mCardProgress == null) { mCard.setProgressType(PROGRESS_TYPE_NO_PROGRESS); } mCard.mCardProgress.maxValue = maxValue; return this; } public Builder setStatus(int status) { mCard.setState(status); return this; } public Card build(Activity activity) { LayoutInflater inflater = activity.getLayoutInflater(); // Inflating the card. ViewGroup cardView = (ViewGroup) inflater.inflate(mCard.mLayoutId, (ViewGroup) activity.findViewById(R.id.card_stream), false); // Check that the layout contains a TextView with the card_title id View viewTitle = cardView.findViewById(R.id.card_title); if (mCard.mTitle != null && viewTitle != null) { mCard.mTitleView = (TextView) viewTitle; mCard.mTitleView.setText(mCard.mTitle); } else if (viewTitle != null) { viewTitle.setVisibility(View.GONE); } // Check that the layout contains a TextView with the card_content id View viewDesc = cardView.findViewById(R.id.card_content); if (mCard.mDescription != null && viewDesc != null) { mCard.mDescView = (TextView) viewDesc; mCard.mDescView.setText(mCard.mDescription); } else if (viewDesc != null) { cardView.findViewById(R.id.card_content).setVisibility(View.GONE); } ViewGroup actionArea = (ViewGroup) cardView.findViewById(R.id.card_actionarea); // Inflate Progress initializeProgressView(inflater, actionArea); // Inflate all action views. initializeActionViews(inflater, cardView, actionArea); mCard.mCardView = cardView; mCard.mOverlayView = cardView.findViewById(R.id.card_overlay); return mCard; } /** * Initialize data from the given card. * @param card * @return * @see Card#createShallowClone() */ public Builder cloneFromCard(Card card) { mCard = card.createShallowClone(); return this; } /** * Build the action views by inflating the appropriate layouts and setting the text and * values. * @param inflater * @param cardView * @param actionArea */ private void initializeActionViews(LayoutInflater inflater, ViewGroup cardView, ViewGroup actionArea) { if (!mCard.mCardActions.isEmpty()) { // Set action area to visible only when actions are visible actionArea.setVisibility(View.VISIBLE); mCard.mActionAreaView = actionArea; } // Inflate all card actions for (final CardAction action : mCard.mCardActions) { int useActionLayout = 0; switch (action.type) { case Card.ACTION_POSITIVE: useActionLayout = R.layout.card_button_positive; break; case Card.ACTION_NEGATIVE: useActionLayout = R.layout.card_button_negative; break; case Card.ACTION_NEUTRAL: default: useActionLayout = R.layout.card_button_neutral; break; } action.actionView = inflater.inflate(useActionLayout, actionArea, false); Button actionButton = (Button) action.actionView.findViewById(R.id.card_button); actionButton.setText(action.label); actionButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mCard.mClickListener.onCardClick(action.id, mCard.mTag); } }); actionArea.addView(action.actionView); } } /** * Build the progress view into the given ViewGroup. * * @param inflater * @param actionArea */ private void initializeProgressView(LayoutInflater inflater, ViewGroup actionArea) { // Only inflate progress layout if a progress type other than NO_PROGRESS was set. if (mCard.mCardProgress != null) { //Setup progress card. View progressView = inflater.inflate(R.layout.card_progress, actionArea, false); ProgressBar progressBar = (ProgressBar) progressView.findViewById(R.id.card_progress); ((TextView) progressView.findViewById(R.id.card_progress_text)) .setText(mCard.mCardProgress.label); progressBar.setMax(mCard.mCardProgress.maxValue); progressBar.setProgress(0); mCard.mCardProgress.progressView = progressView; mCard.mCardProgress.setProgressType(mCard.getProgressType()); actionArea.addView(progressView); } } } /** * Represents a clickable action, accessible from the bottom of the card. * Fields include the label, an ID to specify the action that was performed in the callback, * an action type (positive, negative, neutral), and the callback. */ public class CardAction { public String label; public int id; public int type; public View actionView; public CardAction createShallowClone() { CardAction actionClone = new CardAction(); actionClone.label = label; actionClone.id = id; actionClone.type = type; return actionClone; // Not the view. Never the view (don't want to hold view references for // onConfigurationChange. } } /** * Describes the progress of a {@link Card}. * Three types of progress are supported: * */ public class CardProgress { private int progressType = Card.PROGRESS_TYPE_NO_PROGRESS; private String label = ""; private int currProgress = 0; private int maxValue = 100; public View progressView = null; private ProgressBar progressBar = null; private TextView progressLabel = null; public CardProgress createShallowClone() { CardProgress progressClone = new CardProgress(); progressClone.label = label; progressClone.currProgress = currProgress; progressClone.maxValue = maxValue; progressClone.progressType = progressType; return progressClone; } /** * Set the progress. Only useful for the type {@link #PROGRESS_TYPE_NORMAL}. * @param progress * @see android.widget.ProgressBar#setProgress(int) */ public void setProgress(int progress) { currProgress = progress; final ProgressBar bar = getProgressBar(); if (bar != null) { bar.setProgress(currProgress); bar.invalidate(); } } /** * Set the range of the progress to 0...max. * Only useful for the type {@link #PROGRESS_TYPE_NORMAL}. * @param max * @see android.widget.ProgressBar#setMax(int) */ public void setMax(int max) { maxValue = max; final ProgressBar bar = getProgressBar(); if (bar != null) { bar.setMax(maxValue); } } /** * Set the label text that appears near the progress indicator. * @param text */ public void setProgressLabel(String text) { label = text; final TextView labelView = getProgressLabel(); if (labelView != null) { labelView.setText(text); } } /** * Set how progress is displayed. The parameter must be one of three supported types: *