/**
* Copyright (C) 2013 - 2015 the enviroCar community
*
* This file is part of the enviroCar app.
*
* The enviroCar app is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The enviroCar app is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with the enviroCar app. If not, see http://www.gnu.org/licenses/.
*/
package org.envirocar.app.view.preferences;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.os.Handler;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.FrameLayout;
import com.google.common.base.Preconditions;
import com.squareup.otto.Bus;
import com.squareup.otto.Subscribe;
import org.envirocar.app.R;
import org.envirocar.obd.events.SpeedUpdateEvent;
import org.envirocar.core.logging.Logger;
import org.envirocar.app.views.TypefaceEC;
import javax.inject.Inject;
/**
* @author dewall
*/
public final class Tempomat extends FrameLayout {
private static final Logger LOGGER = Logger.getLogger(Tempomat.class);
// scale configuration
private static final float SCALE_LOWEST_DEGREE = -135.f;
private static final float SCALE_HIGHEST_DEGREE = 135.f;
private static final float SCALE_MAX_DEGREE = 270.f;
private static final float SCALE_MIN_DEGREE = 0.0f;
private static final int SCALE_MIN_SPEED = 0;
private static final int SCALE_MAX_SPEED = 200;
private static final int SCALE_NOTCH_INTERVAL = 5;
private static final int NUM_SPEED_NOTCHES = SCALE_MAX_SPEED / SCALE_NOTCH_INTERVAL;
private static final float DEGREES_PER_NOTCH = SCALE_MAX_DEGREE / NUM_SPEED_NOTCHES;
// Everything related to the animations.
private boolean mCurrentlyAnimating;
private RotationCandidate mNextAnimation;
private RotationCandidate mPrevAnimation;
// The bitmap for the static background and its painter.
private Bitmap mBackground;
private Paint mBackgroundPaint;
// Drawing tools to draw the border
private Paint mBorderPaint;
private Paint mBorderCirclePaint;
private RectF mBorderRect;
private Paint mBorderShadowPaint;
// Drawing tools to draw the background texture of the cruise control stuff.
private RectF mFaceRect;
private Paint mFacePaint;
// Drawing tools to draw the circle ordered scale.
private Paint mScaleTexturePaint;
private Paint mScaleTextPaint;
private RectF mScaleRect;
// Drawing tools for the speed-related text
private TextPaint mSpeedTextPaint;
private TextPaint mUnitTextPaint;
private float mCurrentDegree = -135.0f;
private int mCurrentSpeed = 0;
private SpeedIndicator mSpeedIndicatorView;
private Animation.AnimationListener mAnimationListener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
// Nothing to do..
}
@Override
public void onAnimationEnd(Animation animation) {
synchronized (Tempomat.this) {
mCurrentlyAnimating = false;
executeQueuedAnimation();
}
}
@Override
public void onAnimationRepeat(Animation animation) {
// Nothing to do..
}
};
private Handler mHandler = new Handler();
@Inject
protected Bus mBus;
/**
* Constructor.
*
* @param context the context of the current scope.
*/
public Tempomat(Context context) {
super(context);
init(context);
}
/**
* Constructor.
*
* @param context the context of the current scope.
* @param attrs
*/
public Tempomat(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
/**
* Constructor.
*
* @param context the context of the current scope.
* @param attrs
* @param defStyleAttr
*/
public Tempomat(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
// This FrameLayout view draws something on its own. Therefore, this flag has
// to be set to true.
this.setWillNotDraw(false);
// Initializes the drawing tools.
setupDrawingTools();
// Initialize the SpeedIndicatorView and add it to this framelayout. The
// SpeedIndicatorView will be used to be rotated on the cruise control.
mSpeedIndicatorView = new SpeedIndicator(getContext());
mSpeedIndicatorView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
addView(mSpeedIndicatorView);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// Register on the Bus;
// bus.register(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// Unregister on the bus.
// bus.unregister(this);
}
@Override
protected void onDraw(Canvas canvas) {
Preconditions.checkNotNull(mBackground, "mBackground has not beed initialized before.");
// First, draw the background.
canvas.drawBitmap(mBackground, 0, 0, mBackgroundPaint);
// then draw the current speed in the center.
drawText(canvas);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// LOGGER.info(String.format("onMeasure(%s,%s)", "" + widthMeasureSpec, "" +
// heightMeasureSpec));
// Get the size and mode of width and height.
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// Choose the dimension, width, and height to select.
int chosenWidth = chooseDimension(widthMode, widthSize);
int chosenHeight = chooseDimension(heightMode, heightSize);
int chosenDimension = Math.min(chosenWidth, chosenHeight);
// Store the measured width and measured height.
setMeasuredDimension(chosenDimension, chosenDimension);
// Do the same with the indicator view.
mSpeedIndicatorView.measure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
LOGGER.info("onSizeChanged()");
createBackground();
}
/**
* Receiver method for the speed update event.
*
* @param event the SpeedUpdateEvent to receive over the bus.
*/
@Subscribe
public void onReceiveSpeedUpdateEvent(SpeedUpdateEvent event) {
float degree = 0.0f;
if (event.mSpeed <= SCALE_MIN_SPEED) {
degree = SCALE_MIN_DEGREE;
} else if (event.mSpeed >= SCALE_MAX_SPEED) {
degree = SCALE_MAX_DEGREE;
} else {
degree = (((float) event.mSpeed / SCALE_MAX_SPEED) * SCALE_MAX_DEGREE);
}
mCurrentDegree = degree;
mCurrentSpeed = event.mSpeed;
// Schedule the next rotation to degree.
mHandler.post(() -> submitRotationToDegree(mCurrentDegree));
}
public void setSpeed(int speed) {
float degree = 0.0f;
if (speed <= SCALE_MIN_SPEED) {
degree = SCALE_MIN_DEGREE;
} else if (speed >= SCALE_MAX_SPEED) {
degree = SCALE_MAX_DEGREE;
} else {
degree = (((float) speed / SCALE_MAX_SPEED) * SCALE_MAX_DEGREE);
}
mCurrentDegree = degree;
mCurrentSpeed = speed;
// Schedule the next rotation to degree.
mHandler.post(() -> submitRotationToDegree(mCurrentDegree));
}
private void setupDrawingTools() {
mBorderRect = new RectF(0.1f, 0.1f, 0.9f, 0.9f);
mBackgroundPaint = new Paint();
mBackgroundPaint.setFilterBitmap(true);
// the linear gradient is a bit skewed for realism
mBorderPaint = new Paint();
mBorderPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
mBorderPaint.setShader(new LinearGradient(0.40f, 0.0f, 0.60f, 1.0f,
Color.rgb(0x00, 0x65, 0xA0),
Color.rgb(0x20, 0x20, 0x20),
Shader.TileMode.CLAMP));
mBorderCirclePaint = new Paint();
mBorderCirclePaint.setAntiAlias(true);
mBorderCirclePaint.setStyle(Paint.Style.STROKE);
mBorderCirclePaint.setColor(Color.argb(0x4f, 0x33, 0x36, 0x33));
mBorderCirclePaint.setStrokeWidth(0.005f);
float rimSize = 0.02f;
mFaceRect = new RectF();
mFaceRect.set(
mBorderRect.left + rimSize,
mBorderRect.top + rimSize,
mBorderRect.right - rimSize,
mBorderRect.bottom - rimSize);
mFacePaint = new Paint();
mFacePaint.setFilterBitmap(true);
mFacePaint.setStyle(Paint.Style.FILL);
mFacePaint.setColor(getResources().getColor(R.color.material_blue_grey_950));
mBorderShadowPaint = new Paint();
mBorderShadowPaint.setShader(
new RadialGradient(0.5f, 0.5f,
mFaceRect.width() / 2.0f,
new int[]{0x00000000, 0x00000500, 0x50000500},
new float[]{0.96f, 0.96f, 0.99f},
Shader.TileMode.MIRROR));
mBorderShadowPaint.setStyle(Paint.Style.FILL);
mScaleTexturePaint = new Paint();
mScaleTexturePaint.setStyle(Paint.Style.STROKE);
mScaleTexturePaint.setColor(0x9f048ABF);
mScaleTexturePaint.setStrokeWidth(0.005f);
mScaleTexturePaint.setAntiAlias(true);
mScaleTextPaint = new Paint();
mScaleTextPaint.setStyle(Paint.Style.FILL);
mScaleTextPaint.setColor(0x9f048ABF);
mScaleTextPaint.setStrokeWidth(0.005f);
mScaleTextPaint.setAntiAlias(true);
mScaleTextPaint.setTextSize(1.0f);
mScaleTextPaint.setTypeface(TypefaceEC.Raleway(getContext()));
mScaleTextPaint.setTextScaleX(0.8f);
mScaleTextPaint.setTextAlign(Paint.Align.CENTER);
mScaleTextPaint.setLinearText(true);
float scalePosition = 0.10f;
mScaleRect = new RectF();
mScaleRect.set(
mFaceRect.left + scalePosition,
mFaceRect.top + scalePosition,
mFaceRect.right - scalePosition,
mFaceRect.bottom - scalePosition);
// Unit text painter that holds the settings for the unit of measurement paiting (e.g. km/h)
mUnitTextPaint = new TextPaint();
mUnitTextPaint.setAntiAlias(true);
mUnitTextPaint.setColor(Color.WHITE);
mUnitTextPaint.setStyle(Paint.Style.FILL);
mUnitTextPaint.setTextAlign(Paint.Align.CENTER);
mUnitTextPaint.setTextSize(5f);
mUnitTextPaint.setLinearText(true);
mUnitTextPaint.setTypeface(TypefaceEC.Newscycle(getContext()));
// Text painter for the speed text.
mSpeedTextPaint = new TextPaint();
mSpeedTextPaint.setAntiAlias(true);
mSpeedTextPaint.setColor(Color.WHITE);
mSpeedTextPaint.setStyle(Paint.Style.FILL);
mSpeedTextPaint.setTextSize(20f);
mSpeedTextPaint.setLinearText(true);
mSpeedTextPaint.setTypeface(TypefaceEC.Raleway(getContext()));
mSpeedTextPaint.setShadowLayer(20.f, 0.0f, 0.0f,
getResources().getColor(R.color.blue_light_cario));
}
/**
* @param mode
* @param size
* @return
*/
private int chooseDimension(int mode, int size) {
if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
return size;
} else {
// in case there has not been any size specified.
return 300;
}
}
private void createBackground() {
LOGGER.info("createBackground()");
if (mBackground != null) {
mBackground.recycle();
}
// Create a bitmap for the background we want to draw on.
mBackground = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mBackground);
// Scale the image to the field we focus on.
float scale = (float) getWidth();
canvas.save();
canvas.scale(scale, scale);
// Draw the basic border texture
drawStaticBorderTexture(canvas);
// Draw the general face texture over the border texture
drawStaticFaceTexture(canvas);
// Draw the scaling
drawStaticScaleTexture(canvas);
canvas.restore();
// Other scale required. Some Android versions are not able to deal with scales below 1.0.
canvas.save();
canvas.scale(scale / 100, scale / 100);
drawStaticText(canvas);
canvas.restore();
}
private void drawStaticBorderTexture(final Canvas canvas) {
canvas.drawOval(mBorderRect, mBorderPaint);
canvas.drawOval(mBorderRect, mBorderCirclePaint);
}
private void drawStaticFaceTexture(final Canvas canvas) {
canvas.drawOval(mFaceRect, mFacePaint);
canvas.drawOval(mFaceRect, mBorderCirclePaint);
canvas.drawOval(mFaceRect, mBorderShadowPaint);
}
private void drawStaticScaleTexture(final Canvas canvas) {
canvas.drawOval(mScaleRect, mScaleTexturePaint);
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.rotate(SCALE_LOWEST_DEGREE, 0.5f, 0.5f);
for (int i = 0; i <= NUM_SPEED_NOTCHES; ++i) {
if (i % 4 == 0) {
float y1 = mScaleRect.top;
float y2 = y1 - 0.035f;
canvas.drawLine(0.5f, y1, 0.5f, y2, mScaleTexturePaint);
canvas.save();
canvas.scale(0.045f, 0.045f);
canvas.drawText(Integer.toString(i * 5),
0.5f * 22f, (y2 - 0.010f) * 22f,
mScaleTextPaint);
canvas.restore();
} else {
float y1 = mScaleRect.top;
float y2 = y1 - 0.015f;
canvas.drawLine(0.5f, y1, 0.5f, y2, mScaleTexturePaint);
}
canvas.rotate(DEGREES_PER_NOTCH, 0.5f, 0.5f);
}
canvas.restore();
}
private void drawStaticText(final Canvas canvas) {
LOGGER.info("drawStaticText()");
canvas.drawText("km/h", mScaleRect.centerX() * 100,
mScaleRect.bottom * 100 - 2.8f, mUnitTextPaint);
}
private void drawText(final Canvas canvas) {
float scale = (float) canvas.getWidth() / 100;
canvas.save();
canvas.scale(scale, scale);
Rect textBounds = new Rect();
String valueString = Integer.toString(mCurrentSpeed);
mSpeedTextPaint.getTextBounds(valueString, 0, valueString.length(), textBounds);
canvas.drawText(valueString, 50.0f - textBounds.exactCenterX(),
50.0f - textBounds.centerY(), mSpeedTextPaint);
canvas.restore();
}
/**
* submit a new rotation target degree.
*
* @param degree the target degree
*/
public void submitRotationToDegree(float degree) {
RotationCandidate candidate = new RotationCandidate(degree);
queueAnimation(candidate);
}
/**
* Queues the next animation.
*
* @param anim the rotation candidate to queue.
*/
private synchronized void queueAnimation(RotationCandidate anim) {
mNextAnimation = anim;
if (!mCurrentlyAnimating) {
executeQueuedAnimation();
}
}
/**
* Executes the current queued animation.
*/
private synchronized void executeQueuedAnimation() {
if (mNextAnimation == null)
return;
// Create a new animation from the next rotation holder class.
final Animation anim = createAnimation(mNextAnimation);
if (anim == null)
return;
// Set the animating flag.
mCurrentlyAnimating = true;
// Starts the animation.
mSpeedIndicatorView.startAnimation(anim);
mPrevAnimation = mNextAnimation;
mNextAnimation = null;
}
/**
* Creates a Android-based {@link Animation} from a {@link org.envirocar.app.view.preferences
* .Tempomat.RotationCandidate}.
*
* @param candidate the holder class for the next animation.
* @return a Android animation.
*/
private Animation createAnimation(RotationCandidate candidate) {
float start;
if (mPrevAnimation != null) {
start = mPrevAnimation.finalDegree;
} else {
start = SCALE_MIN_DEGREE;
}
if (start == candidate.finalDegree) {
return null;
}
RotateAnimation anim = new RotateAnimation(start, candidate.finalDegree, Animation
.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
setInterpolatorAndDuration(start, candidate.finalDegree, anim);
anim.setFillAfter(true);
anim.setAnimationListener(mAnimationListener);
return anim;
}
private void setInterpolatorAndDuration(float startDegree, float endDegree, RotateAnimation
anim) {
if (Math.abs(startDegree - endDegree) > 10) {
anim.setInterpolator(new AccelerateDecelerateInterpolator());
anim.setDuration(500);
} else {
anim.setInterpolator(new LinearInterpolator());
anim.setDuration(50);
}
}
/**
* This view class represents the inidcator for speed. The indicator is only drawn once in a
* view
*/
private class SpeedIndicator extends View {
private Bitmap mSpeedIndicatorImage;
private Paint mHandPaint;
private Path mHandPath;
/**
* Constructor.
*
* @param context the context of the current scope.
*/
public SpeedIndicator(Context context) {
super(context);
init();
}
/**
* Constructor.
*
* @param context
* @param attrs
*/
public SpeedIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SpeedIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mHandPaint = new Paint();
mHandPaint.setAntiAlias(true);
mHandPaint.setColor(getResources().getColor(R.color.green_dark_cario));
mHandPaint.setShadowLayer(1.01f, -5.005f, -5.005f, 0x7f000000);
mHandPaint.setStyle(Paint.Style.FILL);
mHandPaint.setStrokeWidth(3);
mHandPath = new Path();
mHandPath.moveTo(0.5f, 0.5f - 0.2f);
mHandPath.lineTo(0.5f - 0.010f, 0.5f - 0.2f - 0.007f);
mHandPath.lineTo(0.5f - 0.002f, 0.5f - 0.32f);
mHandPath.lineTo(0.5f + 0.002f, 0.5f - 0.32f);
mHandPath.lineTo(0.5f + 0.010f, 0.5f - 0.2f - 0.007f);
mHandPath.lineTo(0.5f, 0.5f - 0.2f);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// LOGGER.info(String.format("SpeedIndicator.onMeasure(%s,%s)", "" + widthMeasureSpec, "" +
// heightMeasureSpec));
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int chosenWidth = chooseDimension(widthMode, widthSize);
int chosenHeight = chooseDimension(heightMode, heightSize);
int chosenDimension = Math.min(chosenWidth, chosenHeight);
setMeasuredDimension(chosenDimension, chosenDimension);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
LOGGER.info("SpeedIndicator.onSizeChanged()");
createSpeedIndicator();
}
@Override
protected void onDraw(Canvas canvas) {
Preconditions.checkNotNull(mSpeedIndicatorImage, "mSpeedIndicator cannot be null.");
canvas.drawBitmap(mSpeedIndicatorImage, 0, 0, mBackgroundPaint);
}
private void createSpeedIndicator() {
LOGGER.info("SpeedIndicator.createSpeedIndicator()");
// Recycle the previous speed indicator.
if (mSpeedIndicatorImage != null) {
mSpeedIndicatorImage.recycle();
}
mSpeedIndicatorImage = Bitmap.createBitmap(getWidth(), getHeight(),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mSpeedIndicatorImage);
canvas.save();
float scale = (float) getWidth();
canvas.scale(scale, scale);
canvas.rotate(SCALE_LOWEST_DEGREE, 0.5f, 0.5f);
canvas.drawPath(mHandPath, mHandPaint);
canvas.restore();
}
}
/**
* Holder class that holds candidate for the next rotation animation.
*/
private class RotationCandidate {
private float finalDegree;
public RotationCandidate(float degree) {
if (degree < SCALE_LOWEST_DEGREE) {
this.finalDegree = SCALE_MIN_DEGREE;
} else if (degree > SCALE_MAX_DEGREE) {
this.finalDegree = SCALE_MAX_DEGREE;
} else {
this.finalDegree = degree;
}
}
}
}