/*
* Copyright (c) 2013 Allogy Interactive.
*
* 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.allogy.app.ui;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import com.allogy.app.R;
import com.allogy.app.media.OnSeekListener;
import com.allogy.app.util.Util;
/**
* <h1>
* AnnotatedProgressBar</h1>
* <p>
* Displays the progress of a process as well as enables the display of
* annotation marks for specific occurrences in the progress time line.
* </p>
*
* @author Diego Nunez
*/
public class AnnotatedProgressBar extends View {
// TODO: Move any hard coded string references into the string.xml resource
// and reference them from there.
// /
// / PROPERTIES
// /
public static final String LOG_TAG = "AnnotatedProgressBar";
/**
* <p>
* Default value provided if the display dimensions of the View is not
* specified as a percent.
* </p>
*/
public static final int PERCENT_UNSPECIFIED = -1;
/**
*
**/
public static final int DEFAULT_ANNOTATION_HEIGHT = 10;
/**
* <p>
* </p>
**/
public static final int DEFAULT_ANNOTATION_WIDTH = 1;
/**
*
*/
public static final int DEFAULT_MAX_PROGRESS = 100;
/**
*
*/
public static final int DEFAULT_MIN_PROGRESS = 0;
/**
*
*/
public static final int DEFAULT_SLIDER_RADIUS = 10;
/**
*
**/
public static final int DEFAULT_VIEW_HEIGHT_PADDING = 5;
/**
* <p>
* Image resource for the <b>AnnotatedProgressBar</b> View.
* </p>
*/
private final Bitmap mAnnotationBitmap;
private final int mAnnotationWidth, mAnnotationHeight;
/**
* <p>
* </p>
*/
private Bitmap mBackgroundBitmap;
/**
* <p>
*
* </p>
*/
private final AnnotatedProgressBarSlider mSlider;
/**
* <p>
* The percentage for which to calculate the display dimension for the View.
* The values can range from 0 - 1.
* </p>
*/
private final float percentWidth, percentHeight;
/**
* <p>
* </p>
*/
private int minProgress, maxProgress, currentProgress;
/**
* <p>
* </p>
**/
private boolean isTouched = false;
/**
* <p>
* </p>
*/
private List<AnnotationItem> mChildAnnotations = new ArrayList<AnnotationItem>();
/**
* <p>
* </p>
**/
private OnSeekListener mSeekListener;
/**
*
*/
private int mWidth = 0;
// /
// / CONSTRUCTORS
// /
/**
* Initializes a new instace of <b>AnnotatedProgressBar</b>.
*
* @param context
* The Context the view is running in, through which it can
* access the current theme, resources, etc.
*/
public AnnotatedProgressBar(Context context) {
super(context);
mSlider = null;
mAnnotationBitmap = null;
mBackgroundBitmap = null;
percentWidth = PERCENT_UNSPECIFIED;
percentHeight = PERCENT_UNSPECIFIED;
minProgress = DEFAULT_MIN_PROGRESS;
maxProgress = DEFAULT_MAX_PROGRESS;
mAnnotationWidth = DEFAULT_ANNOTATION_WIDTH;
mAnnotationHeight = DEFAULT_ANNOTATION_HEIGHT;
// TODO: Enable for programmatic creation of the View. If it's not
// neccesary, then delete this constructor.
}
/**
* <p>
* Initializes a new instace of <b>AnnotatedProgressBar</b>.
* </p>
* <p>
* Constructor that is called when inflating a view from XML. This is called
* when a view is being constructed from an XML file, supplying attributes
* that were specified in the XML file. This version uses a default style of
* 0, so the only attribute values applied are those in the Context's Theme
* and the given AttributeSet. The method onFinishInflate() will be called
* after all children have been added.
* </p>
*
* @param context
* The Context the view is running in, through which it can
* access the current theme, resources, etc.
* @param attrs
* The attributes of the XML tag that is inflating the view.
*/
public AnnotatedProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
minProgress = DEFAULT_MIN_PROGRESS;
maxProgress = DEFAULT_MAX_PROGRESS;
Drawable tempDrawable = null;
Bitmap tempBmp = null;
int tempWidth = 0, tempHeight = 0;
// retrieve the attributes from the inflated layout XML.
RuntimeException re = null;
TypedArray typedArr = context.obtainStyledAttributes(attrs,
R.styleable.AnnotatedProgressBar);
percentWidth = typedArr.getInt(
R.styleable.AnnotatedProgressBar_body_percent_width,
PERCENT_UNSPECIFIED);
percentHeight = typedArr.getInt(
R.styleable.AnnotatedProgressBar_body_percent_width,
PERCENT_UNSPECIFIED);
// initialize the slider resources.
mSlider = new AnnotatedProgressBarSlider();
if (null != (tempDrawable = typedArr
.getDrawable(R.styleable.AnnotatedProgressBar_slider_background))) {
mSlider.mBackgroundBitmap = ((BitmapDrawable) tempDrawable)
.getBitmap();
mSlider.height = mSlider.mBackgroundBitmap.getHeight();
mSlider.width = mSlider.height / 2;
} else {
// disregard because the slider background resource is not required.
mSlider.mBackgroundBitmap = null;
}
if (null != (tempDrawable = typedArr
.getDrawable(R.styleable.AnnotatedProgressBar_slider_handler))) {
mSlider.mHandlerBitmap = ((BitmapDrawable) tempDrawable)
.getBitmap();
mSlider.width = mSlider.mHandlerBitmap.getWidth();
mSlider.height = mSlider.mHandlerBitmap.getHeight();
} else {
mSlider.mHandlerBitmap = null;
}
if (null != (tempDrawable = typedArr
.getDrawable(R.styleable.AnnotatedProgressBar_annotation_src))) {
tempBmp = ((BitmapDrawable) tempDrawable).getBitmap();
tempWidth = tempBmp.getWidth();
tempHeight = tempBmp.getHeight();
} else {
// disregard because the annotation resource is not required.
tempBmp = null;
tempWidth = DEFAULT_ANNOTATION_WIDTH;
tempHeight = DEFAULT_ANNOTATION_HEIGHT;
}
mAnnotationBitmap = tempBmp;
mAnnotationWidth = tempWidth;
mAnnotationHeight = tempHeight;
if (null != (tempDrawable = typedArr
.getDrawable(R.styleable.AnnotatedProgressBar_body_background))) {
tempBmp = ((BitmapDrawable) tempDrawable).getBitmap();
} else {
tempBmp = null;
}
mBackgroundBitmap = tempBmp;
// perform clean up.
typedArr.recycle();
if (null != re) {
throw re;
}
}
/**
* Initializes a new instace of <b>AnnotatedProgressBar</b>.
* <p>
* Perform inflation from XML and apply a class-specific base style. This
* constructor of View allows subclasses to use their own base style when
* they are inflating. For example, a Button class's constructor would call
* this version of the super class constructor and supply R.attr.buttonStyle
* for defStyle; this allows the theme's button style to modify all of the
* base view attributes (in particular its background) as well as the Button
* class's attributes.
* </p>
*
* @param context
* The Context the view is running in, through which it can
* access the current theme, resources, etc.
* @param attrs
* The attributes of the XML tag that is inflating the view.
* @param defStyle
* The default style to apply to this view. If 0, no style will
* be applied (beyond what is included in the theme). This may
* either be an attribute resource, whose value will be retrieved
* from the current theme, or an explicit style resource.
*/
public AnnotatedProgressBar(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
mSlider = null;
mAnnotationBitmap = null;
mBackgroundBitmap = null;
minProgress = DEFAULT_MIN_PROGRESS;
maxProgress = DEFAULT_MAX_PROGRESS;
mAnnotationWidth = DEFAULT_ANNOTATION_WIDTH;
mAnnotationHeight = DEFAULT_ANNOTATION_HEIGHT;
// TODO: Retrieve the desired attributes from the attribute set. This
// might not be nessesary and could be deleted. It might just be a copy
// of the other constructor.
RuntimeException re = null;
TypedArray typedArr = context.obtainStyledAttributes(attrs,
R.styleable.AnnotatedProgressBar);
percentWidth = typedArr.getInt(
R.styleable.AnnotatedProgressBar_body_percent_width,
PERCENT_UNSPECIFIED);
percentHeight = typedArr.getInt(
R.styleable.AnnotatedProgressBar_body_percent_width,
PERCENT_UNSPECIFIED);
// perform clean up.
typedArr.recycle();
if (null != re) {
throw re;
}
}
// /
// / VIEW METHODS
// /
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
this.mWidth = MeasureWidth(widthMeasureSpec);
setMeasuredDimension(this.mWidth, MeasureHeight(heightMeasureSpec));
}
/*
* The View has three parts to it, the background, the slider, and the
* annotations, which are rendered in exactly that order.
*/
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
// TODO: Probably good to move this into it's own function.
// Render the background.
if (null != mBackgroundBitmap && !mBackgroundBitmap.isRecycled()) {
if (mBackgroundBitmap.getWidth() != this.mWidth
|| mBackgroundBitmap.getHeight() != this.getHeight()) {
Bitmap temp = Bitmap.createScaledBitmap(mBackgroundBitmap, this
.getWidth(), this.getHeight(), false);
mBackgroundBitmap = null;
mBackgroundBitmap = temp;
}
canvas.drawBitmap(mBackgroundBitmap, 0, 0, null);
} else {
// Default.
Paint rec = new Paint();
rec.setAntiAlias(true);
rec.setColor(Color.RED);
canvas.drawRect(0, 0, this.mWidth, this.getHeight(), rec);
}
RenderSlider(canvas);
RenderAnnotations(canvas);
return;
}
/**
* Draws the slider component of the View to the screen.
*
* @param canvas
* The target to draw upon.
*/
private void RenderSlider(Canvas canvas) {
float xprog = TransformProgressTargetToCanvas(currentProgress);
// Draw the desired background or a line that the slider will
// slide over.
if (null != mSlider.mBackgroundBitmap
&& !mSlider.mBackgroundBitmap.isRecycled()) {
if (mSlider.mBackgroundBitmap.getWidth() != this.mWidth) {
Bitmap temp = Bitmap.createScaledBitmap(
mSlider.mBackgroundBitmap, this.mWidth,
mSlider.mBackgroundBitmap.getHeight(), true);
mSlider.mBackgroundBitmap = null;
mSlider.mBackgroundBitmap = temp;
}
canvas.drawBitmap(mSlider.mBackgroundBitmap, 0, this.getHeight()
- mSlider.height, null);
} else {
// Default.
Paint line = new Paint();
line.setAntiAlias(true);
line.setColor(Color.BLACK);
canvas
.drawLine(0, this.getHeight() - DEFAULT_SLIDER_RADIUS, this
.getWidth(), this.getHeight()
- DEFAULT_SLIDER_RADIUS, line);
}
if (null != mSlider.mHandlerBitmap
&& !mSlider.mHandlerBitmap.isRecycled()) {
canvas.drawBitmap(mSlider.mHandlerBitmap, xprog, this.getHeight()
- mSlider.height, null);
} else {
Paint circleFill = new Paint();
circleFill.setAntiAlias(true);
circleFill.setColor(Color.LTGRAY);
Paint circleBorder = new Paint();
circleBorder.setAntiAlias(true);
circleBorder.setColor(Color.BLACK);
canvas.drawCircle(xprog + mSlider.width, this.getHeight()
- mSlider.width, mSlider.width, circleBorder);
canvas.drawCircle(xprog + mSlider.width, this.getHeight()
- mSlider.width, mSlider.width - 2, circleFill);
}
}
/**
* Draws the annotation componet of the View to the screen.
*
* @param canvas
* The target to draw upon.
*/
private void RenderAnnotations(Canvas canvas) {
if (null != mAnnotationBitmap) {
for (int i = 0, len = mChildAnnotations.size(); i < len; i++) {
AnnotationItem item = mChildAnnotations.get(i);
if (!item.mValid && !item.Validate()) {
continue;
}
canvas.drawBitmap(mAnnotationBitmap, item.mDisplayProgress, 0,
null);
}
} else {
// Default.
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.YELLOW);
for (int i = 0, len = mChildAnnotations.size(); i < len; i++) {
AnnotationItem item = mChildAnnotations.get(i);
Log.i(LOG_TAG, "Got the annotation item " + i + " : " + item.mTrueProgress);
if (!item.mValid && !item.Validate()) {
continue;
}
Log.i(LOG_TAG, "Drawing the annotation line");
canvas.drawLine(item.mDisplayProgress, 0,
item.mDisplayProgress, mAnnotationHeight, paint);
}
}
}
// /
// / VIEW EVENTS
// /
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isTouched = true;
if (null != mSeekListener) {
mSeekListener.onSeekStarted(currentProgress);
}
break;
case MotionEvent.ACTION_MOVE:
CalculateSeek(event.getX());
if (null != mSeekListener) {
mSeekListener.onSeeking();
}
break;
case MotionEvent.ACTION_OUTSIDE:
isTouched = false;
Log.e(this.getClass().getName(), "Out of bounds!");
break;
case MotionEvent.ACTION_UP:
CalculateSeek(event.getX());
isTouched = false;
if (currentProgress > maxProgress) {
currentProgress = maxProgress;
} else if (currentProgress < minProgress) {
currentProgress = minProgress;
}
if (null != mSeekListener) {
mSeekListener.onSeekFinished(currentProgress);
}
break;
default:
// other events are ignored.
super.onTouchEvent(event);
return false;
}
return true;
}
// /
// / PRIVATE METHODS
// /
/**
* Determines the correct width to measure the View.
*
* @param measureSpec
* The <b>MeasureSpec</b> of the width.
* @return The correct display width.
*/
private int MeasureWidth(int measureSpec) {
int result = 0, specSize = MeasureSpec.getSize(measureSpec);
if (percentWidth > 0) {
DisplayMetrics dm = new DisplayMetrics();
((WindowManager) this.getContext().getSystemService(
Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(dm);
result = Util.percentOf(dm.widthPixels, percentWidth);
} else {
result = specSize;
}
return result;
}
/**
* Determines the correct height to measure the View.
*
* @param measureSpec
* The <b>MeasureSpec</b> of the height.
* @return The correct display height.
*/
private int MeasureHeight(int measureSpec) {
int result = 0, specSize = MeasureSpec.getSize(measureSpec);
int temp = 0;
if (percentHeight > 0) {
DisplayMetrics dm = new DisplayMetrics();
((WindowManager) this.getContext().getSystemService(
Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(dm);
result = Util.percentOf(dm.heightPixels, percentHeight);
} else {
result = specSize;
}
if (result < (temp = mAnnotationHeight + mSlider.height)) {
result = temp;
}
return result + DEFAULT_VIEW_HEIGHT_PADDING;
}
/**
* Calculate the appropriate progress from the position of the touch event.
*
* @param touchX
* The posision of the touch event.
*/
private void CalculateSeek(float touchX) {
currentProgress = TransformProgressCanvasToTarget(touchX);
this.invalidate();
}
/**
* <p>
* Xp = Pcurr * ( Xmax - Xmin) / ( Pmax - Pmin)
* </p>
*
* @param canvasProgress
* The current progress as drawn on the canvas.
* @return The current progress in milliseconds.
*/
private int TransformProgressCanvasToTarget(float canvasProgress) {
return (int) (canvasProgress * (maxProgress - minProgress) / (this
.getWidth()
- mSlider.width - mSlider.width));
}
/**
* <p>
* Pcurr = Xp * ( Pmax - Pmin) / ( Xmax - Xmin)
* </p>
*
* @param targetProgess
* The current progress in milliseconds.
* @return The current progress to be drawn to the canvas.
*/
private float TransformProgressTargetToCanvas(int targetProgess) {
int xmax = this.mWidth, xmin = mSlider.width;
return targetProgess * (float) (xmax - xmin)
/ (float) (maxProgress - minProgress);
}
// /
// / METHODS
// /
/**
* Checks to see if the View has any annotations.
*/
public boolean HasAnnotations() {
return mChildAnnotations.isEmpty();
}
/**
* Setter for providing a handler for seek events.
*
* @param listener
* The handler that will handle seek events.
*/
public void SetOnSeekListener(OnSeekListener listener) {
mSeekListener = listener;
}
/**
* Getter for the current progress.
*
* @return The current progress.
*/
public int GetProgress() {
return this.currentProgress;
}
/**
* Setter for the current progress.
*
* @param val
* The new progress.
*/
public int SetProgress(int val) {
if (isTouched) {
return currentProgress;
}
if (val < minProgress) {
currentProgress = minProgress;
} else if (val > maxProgress) {
currentProgress = maxProgress;
} else {
currentProgress = val;
}
this.invalidate();
return currentProgress;
}
/**
* Setter for the minimum progress value.
*
* @param val
* The new minimum value.
*/
public void SetMinProgress(int val) {
if (val < DEFAULT_MIN_PROGRESS) {
minProgress = DEFAULT_MIN_PROGRESS;
} else if (val >= DEFAULT_MAX_PROGRESS || val >= maxProgress) {
minProgress = DEFAULT_MIN_PROGRESS;
} else {
minProgress = val;
}
this.invalidate();
}
/**
* Setter for the maximum progress value.
*
* @param val
* The new maximum value.
*/
public void SetMaxProgress(int val) {
if (val <= DEFAULT_MIN_PROGRESS || val <= minProgress) {
maxProgress = DEFAULT_MAX_PROGRESS;
minProgress = DEFAULT_MIN_PROGRESS;
} else {
maxProgress = val;
}
this.invalidate();
}
/**
* Adds an annotation to the View.
*
* @param progress
* The position with respect to the time line of the new
* annotation.
* @return <p>
* True if the annotation was added successfully, false otherwise.
* </p>
*/
public boolean AddAnnotation(int progress) {
AnnotationItem item = new AnnotationItem(progress);
if (!mChildAnnotations.contains(item)) {
mChildAnnotations.add(item);
this.invalidate();
return true;
}
return false;
}
/**
* Removes an annotation from the View.
*
* @param progress
* The position with respect to the time line of the existing
* annoation.
* @return <p>
* True if the annotation was deleted successfully, false otherwise.
* </p>
*/
public boolean RemoveAnnotation(int progress) {
AnnotationItem item = new AnnotationItem(progress);
if (mChildAnnotations.contains(item)) {
mChildAnnotations.remove(item);
this.invalidate();
return true;
}
return false;
}
/**
* Removes all annotations from the View.
*/
public void ClearAnnotations() {
mChildAnnotations.clear();
this.invalidate();
}
// /
// / INTERNAL CLASSES
// /
/**
* Represents a single annotation.
*/
private class AnnotationItem {
public int mTrueProgress;
public float mDisplayProgress;
public boolean mValid;
/**
* Initializes a new instace of <b>AnnotationItem</b>.
*
* @param progress
* @param valid
*/
public AnnotationItem(int progress) {
mTrueProgress = progress;
mValid = (mDisplayProgress = TransformProgressTargetToCanvas(progress)) >= 0;
}
/**
* Checks to make sure the annotation is valid.
*/
public boolean Validate() {
return (mValid = (mDisplayProgress = TransformProgressTargetToCanvas(this.mTrueProgress)) >= 0);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof AnnotationItem) {
return this.mTrueProgress == ((AnnotationItem) obj).mTrueProgress;
} else {
return false;
}
}
@Override
public int hashCode() {
return this.mTrueProgress;
}
}
/**
* Represents the slider of the <b>AnnotationProgressBar</b>.
*/
private class AnnotatedProgressBarSlider {
public int width, height;
public Bitmap mHandlerBitmap;
public Bitmap mBackgroundBitmap;
/**
* Initializes a new instance of <b>AnnotationProgressBarSlider</b>.
*/
public AnnotatedProgressBarSlider() {
height = width = DEFAULT_SLIDER_RADIUS;
mBackgroundBitmap = mHandlerBitmap = null;
}
}
}