/*
* Copyright (C) 2013 Manuel Peinado
*
* 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.manuelpeinado.refreshactionitem;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Toast;
import com.readystatesoftware.viewbadger.BadgeView;
/**
* An action bar item implementing a common pattern: initially a refresh button
* is shown, and when the button is clicked a background operation begins and
* the button turns into a progress indicator until the operation ends, at which
* point the button is restored to its initial state.
* <p/>
* The progress indicator can be determinate or indeterminate. If the
* determinate mode is used, it is possible to choose between two styles:
* "wheel" and "pie".
* <p/>
* It is also possible to have the refresh button be invisible initially, which
* makes this action item behave like a replacement for the built-in
* indeterminate action bar progress indicator (with the benefit that with this
* action item the progress can be determinate).
* <p/>
* The action item also supports adding a small badge that indicates that there
* is new data available.
*/
public class RefreshActionItem extends FrameLayout implements OnClickListener, OnLongClickListener {
private final ImageView mRefreshButton;
private final ProgressBar mProgressIndicatorIndeterminate;
private final ProgressIndicator mProgressIndicator;
private RefreshActionListener mRefreshButtonListener;
private BadgeView mBadge;
private int mBadgeBackgroundColor = -1;
private int mBadgeTextStyle;
private int mBadgePosition;
// Please note that the state can be "showing progress" and "showing badge"
// simultaneously, in that case
// the badge remains hidden until we stop showing progress
private boolean mShowingBadge;
private MenuItem mMenuItem;
private boolean mShowingProgress;
private int mMax = 100;
private int mProgress = 0;
private ProgressIndicatorType mProgressIndicatorType;
public RefreshActionItem(Context context) {
this(context, null);
}
public RefreshActionItem(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.refreshActionItemStyle);
}
@SuppressWarnings("deprecation")
public RefreshActionItem(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.rai__action_item, this);
mRefreshButton = (ImageView) findViewById(R.id.refresh_button);
mRefreshButton.setOnClickListener(this);
mRefreshButton.setOnLongClickListener(this);
mProgressIndicatorIndeterminate = (ProgressBar) findViewById(R.id.indeterminate_progress_indicator);
mProgressIndicator = (ProgressIndicator) findViewById(R.id.determinate_progress_indicator);
updateChildrenVisibility();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshActionItem, defStyle, R.style.Widget_RefreshActionItem_Dark);
int N = a.getIndexCount();
for (int i = 0; i < N; ++i) {
int attr = a.getIndex(i);
switch (attr) {
// case R.styleable.RefreshActionItem_progressIndicatorType:
// mProgressIndicatorType = ProgressIndicatorType.values()[a.getInt(attr, 0)];
// if (mProgressIndicatorType == ProgressIndicatorType.PIE) {
// mProgressIndicator.setPieStyle(true);
// }
// break;
// case R.styleable.RefreshActionItem_refreshActionItemIcon:
// Drawable refreshButtonIcon = a.getDrawable(attr);
// mRefreshButton.setImageDrawable(refreshButtonIcon);
// break;
// case R.styleable.RefreshActionItem_progressIndicatorForegroundColor:
// int color = a.getColor(attr, 0);
// mProgressIndicator.setForegroundColor(color);
// break;
// case R.styleable.RefreshActionItem_progressIndicatorBackgroundColor:
// color = a.getColor(attr, 0);
// mProgressIndicator.setBackgroundColor(color);
// break;
// case R.styleable.RefreshActionItem_refreshActionItemBackground:
// Drawable drawable = a.getDrawable(attr);
// mRefreshButton.setBackgroundDrawable(drawable);
// break;
// case R.styleable.RefreshActionItem_badgeBackgroundColor:
// mBadgeBackgroundColor = a.getColor(attr, -1);
// break;
// case R.styleable.RefreshActionItem_badgeTextStyle:
// mBadgeTextStyle = a.getResourceId(attr, 0);
// break;
// case R.styleable.RefreshActionItem_badgePosition:
// mBadgePosition = a.getInt(attr, 0);
// break;
}
}
a.recycle();
}
public void setRefreshActionListener(RefreshActionListener listener) {
this.mRefreshButtonListener = listener;
}
public MenuItem getMenuItem() {
return mMenuItem;
}
public void setMenuItem(MenuItem menuItem) {
this.mMenuItem = menuItem;
if (menuItem.getIcon() != null) {
mRefreshButton.setImageDrawable(mMenuItem.getIcon());
}
}
/**
* Adds an exclamation icon to the refresh button. This is intended to
* suggest the user that new data is available.
* <p/>
* The badge is only shown in the {@link BUTTON} display mode
* <p/>
* If the display mode is not <tt>BUTTON</tt>, nothing is done.
*
* @see #showBadge(String)
*/
public void showBadge() {
showBadge("!");
}
/**
* Adds a badge icon with a give text to the refresh button. This is
* intended to suggest the user that new data is available, including how
* many items
* <p/>
* The badge is only shown in the {@link BUTTON} display mode
* <p/>
* If the display mode is not <tt>BUTTON</tt>, nothing is done.
*
* @param text Text that is drawn inside the badge icon
* @see #showBadge()
*/
public void showBadge(String text) {
hideBadge();
if (mBadge == null) {
mBadge = new BadgeView(getContext(), mRefreshButton);
mBadge.setBadgePosition(mBadgePosition);
if (mBadgeTextStyle != 0) {
mBadge.setTextAppearance(getContext(), mBadgeTextStyle);
}
if (mBadgeBackgroundColor != -1) {
mBadge.setBadgeBackgroundColor(mBadgeBackgroundColor);
}
}
mShowingBadge = true;
mBadge.setText(text);
if (!mShowingProgress) {
// Otherwise the badge will be shown as soon as we stop showing
// progress
mBadge.show(true);
}
}
/**
* Hides the badge associated to this action item.
* <p/>
* If the display mode is not <tt>BUTTON</tt> or the badge is not visible,
* nothing is done.
*
* @see #showBadge()
* @see #showBadge(String)
* @see #isBadgeVisible()
*/
public void hideBadge() {
if (mBadge == null || !mShowingBadge) {
return;
}
mShowingBadge = false;
if (!mShowingProgress) {
// If showing progress the badge is already hidden
mBadge.hide(true);
}
}
/**
* Returns whether this action item has a visible badge.
*
* @see #showBadge()
* @see #showBadge(String)
* @see #hideBadge()
*/
public boolean isBadgeVisible() {
return mBadge != null && mShowingBadge;
}
private void updateChildrenVisibility() {
if (!mShowingProgress) {
mRefreshButton.setVisibility(View.VISIBLE);
mProgressIndicatorIndeterminate.setVisibility(View.GONE);
mProgressIndicator.setVisibility(View.GONE);
return;
}
if (mProgressIndicatorType == ProgressIndicatorType.INDETERMINATE) {
mRefreshButton.setVisibility(View.GONE);
mProgressIndicatorIndeterminate.setVisibility(View.VISIBLE);
mProgressIndicator.setVisibility(View.GONE);
updateProgressIndicatorValue();
return;
}
mRefreshButton.setVisibility(View.GONE);
mProgressIndicatorIndeterminate.setVisibility(View.GONE);
mProgressIndicator.setVisibility(View.VISIBLE);
updateProgressIndicatorValue();
}
/**
* Return the upper limit of this progress bar's range.
*
* @return a positive integer
* @see #setMax(int)
* @see #getProgress()
*/
public synchronized int getMax() {
return mMax;
}
/**
* Set the range of the progress bar to 0...<tt>max</tt>
*
* @param max the upper range of this progress bar
* @see #getMax()
* @see #setProgress(int)
*/
public synchronized void setMax(int max) {
if (max < 0) {
max = 0;
}
if (max != mMax) {
mMax = max;
if (mProgress > mMax) {
mProgress = mMax;
}
updateProgressIndicatorValue();
}
}
/**
* Changes the state of the action item between the modes
* "showing refresh button" and "showing progress"
*
* @param show
*/
public void showProgress(boolean show) {
if (show == mShowingProgress) {
return;
}
if (isBadgeVisible()) {
if (show) {
// Hide badge temporarily until we stop showing progress
mBadge.hide(false);
} else {
// If badge was hidden temporarily we restore it back to visible
mBadge.show(false);
}
}
setProgress(0);
mShowingProgress = show;
updateChildrenVisibility();
}
/**
* Set the current progress to the specified value. If the progress bar is
* not in determinate mode the view is not changed.
*
* @param progress the new progress, between 0 and {@link #getMax()}
* @see #setDisplayMode(DisplayMode)
* @see #getMode()
* @see #getProgress()
* @see #incrementProgressBy(int)
*/
public synchronized void setProgress(int progress) {
if (progress < 0) {
progress = 0;
}
if (progress > mMax) {
progress = mMax;
}
if (progress != mProgress) {
mProgress = progress;
updateProgressIndicatorValue();
}
}
private void updateProgressIndicatorValue() {
mProgressIndicator.setValue(mProgress / (float) mMax);
}
/**
* Increase the progress bar's progress by the specified amount.
*
* @param diff the amount by which the progress must be increased
* @see #setProgress(int)
*/
public synchronized final void incrementProgressBy(int diff) {
setProgress(mProgress + diff);
}
public ProgressIndicatorType getProgressIndicatorType() {
return mProgressIndicatorType;
}
/**
* This has no effect if the action item has indeterminate progress
*
* @param style One of {@link ProgressIndicatorType#WHEEL},
* {@link ProgressIndicatorType#PIE} or
* {@link ProgressIndicatorType#INDETERMINATE}
*/
public void setProgressIndicatorType(ProgressIndicatorType style) {
if (style == mProgressIndicatorType) {
return;
}
mProgressIndicatorType = style;
if (style == ProgressIndicatorType.PIE) {
mProgressIndicator.setPieStyle(true);
} else if (style == ProgressIndicatorType.WHEEL) {
mProgressIndicator.setPieStyle(false);
}
updateChildrenVisibility();
}
@Override
public void onClick(View v) {
if (mRefreshButtonListener != null) {
mRefreshButtonListener.onRefreshButtonClick(this);
}
}
@Override
public boolean onLongClick(View v) {
if (mMenuItem == null || TextUtils.isEmpty(mMenuItem.getTitle())) {
return true;
}
final int[] screenPos = new int[2];
final Rect displayFrame = new Rect();
getLocationOnScreen(screenPos);
getWindowVisibleDisplayFrame(displayFrame);
final Context context = getContext();
final int width = getWidth();
final int height = getHeight();
final int midy = screenPos[1] + height / 2;
final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
Toast cheatSheet = Toast.makeText(context, mMenuItem.getTitle(), Toast.LENGTH_SHORT);
if (midy < displayFrame.height()) {
cheatSheet.setGravity(Gravity.TOP | Gravity.RIGHT, screenWidth - screenPos[0] - width / 2, height);
} else {
cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
}
cheatSheet.show();
return true;
}
public interface RefreshActionListener {
void onRefreshButtonClick(RefreshActionItem sender);
}
}