/*
* 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<CardAction> mCardActions = new ArrayList<CardAction>();
/**
* 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:
* <ul>
* <li>{@link #PROGRESS_TYPE_NO_PROGRESS} disables the progress indicator</li>
* <li>{@link #PROGRESS_TYPE_NORMAL}
* displays a standard, linear progress indicator.</li>
* <li>{@link #PROGRESS_TYPE_INDETERMINATE} displays an indeterminate (infite) progress
* indicator.</li>
* <li>{@link #PROGRESS_TYPE_LABEL} only displays a label text in the progress area
* of the card.</li>
* </ul>
*
* @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:
* <ul><li>{@link Card#PROGRESS_TYPE_NORMAL: Standard progress bar with label text</li>
* <li>{@link Card#PROGRESS_TYPE_INDETERMINATE}: Indeterminate progress bar with label txt</li>
* <li>{@link Card#PROGRESS_TYPE_LABEL}: Label only, no progresss bar</li>
* </ul>
*/
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:
* <ul><li>{@link Card#PROGRESS_TYPE_NORMAL: Standard progress bar with label text</li>
* <li>{@link Card#PROGRESS_TYPE_INDETERMINATE}:
* Indeterminate progress bar with label txt</li>
* <li>{@link Card#PROGRESS_TYPE_LABEL}: Label only, no progresss bar</li>
* @param type
*/
public void setProgressType(int type) {
progressType = type;
if (progressView != null) {
switch (type) {
case PROGRESS_TYPE_NO_PROGRESS: {
progressView.setVisibility(View.GONE);
break;
}
case PROGRESS_TYPE_NORMAL: {
progressView.setVisibility(View.VISIBLE);
getProgressBar().setIndeterminate(false);
break;
}
case PROGRESS_TYPE_INDETERMINATE: {
progressView.setVisibility(View.VISIBLE);
getProgressBar().setIndeterminate(true);
break;
}
}
}
}
private TextView getProgressLabel() {
if (progressLabel != null) {
return progressLabel;
} else if (progressView != null) {
progressLabel = (TextView) progressView.findViewById(R.id.card_progress_text);
return progressLabel;
} else {
return null;
}
}
private ProgressBar getProgressBar() {
if (progressBar != null) {
return progressBar;
} else if (progressView != null) {
progressBar = (ProgressBar) progressView.findViewById(R.id.card_progress);
return progressBar;
} else {
return null;
}
}
}
}