Weird flickering with DashPathEffect animation - android

I'm trying to make a path animation with DashPathEffect. Have to be able to replay it multiple times, using a button.
Based on http://www.curious-creature.org/2013/12/21/android-recipe-4-path-tracing/
My problem is, the animation works well the first 10 times or so, then the path goes crazy and it starts flickering, going in the reverse order or finishes where it doesn't have to.
I logged the values of the animation and the values passed to the path and they look correct, no jumps or anything weird. I have no idea what is causing the issues.
This is the code:
public class Test extends View {
private float mDrag;
private MyPath path1;
private int mDuration;
//just some shape
private static Path makeDragPath(int radius) {
Path p = new Path();
RectF oval = new RectF(10.0f, 10.0f, radius * 2.0f, radius * 2.0f);
float cx = oval.centerX();
float cy = oval.centerY();
float rx = oval.width() / 2.0f;
float ry = oval.height() / 2.0f;
final float TAN_PI_OVER_8 = 0.414213562f;
final float ROOT_2_OVER_2 = 0.707106781f;
float sx = rx * TAN_PI_OVER_8;
float sy = ry * TAN_PI_OVER_8;
float mx = rx * ROOT_2_OVER_2;
float my = ry * ROOT_2_OVER_2;
float L = oval.left;
float T = oval.top;
float R = oval.right;
float B = oval.bottom;
p.moveTo(R, cy);
p.quadTo( R, cy + sy, cx + mx, cy + my);
p.quadTo(cx + sx, B, cx, B);
p.quadTo(cx - sx, B, cx - mx, cy + my);
p.quadTo(L, cy + sy, L, cy);
p.quadTo( L, cy - sy, cx - mx, cy - my);
p.quadTo(cx - sx, T, cx, T);
return p;
}
public Test(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public static class MyPath {
private static final Region sRegion = new Region();
private static final Region sMaxClip = new Region(
Integer.MIN_VALUE, Integer.MIN_VALUE,
Integer.MAX_VALUE, Integer.MAX_VALUE);
final Path path;
final Paint paint;
final float length;
final Rect bounds;
MyPath(Path path, Paint paint) {
this.path = path;
this.paint = paint;
PathMeasure measure = new PathMeasure(path, false);
this.length = measure.getLength();
sRegion.setPath(path, sMaxClip);
bounds = sRegion.getBounds();
}
}
private static PathEffect createPathEffect2(float pathLength, float phase) {
//I modified the original approach using phase, to use only path instead because later I want to animate also the starting point and phase alone is not enough for this
float full = phase * pathLength;
return new DashPathEffect(new float[] {full, Float.MAX_VALUE}, //on, off
0);
}
ObjectAnimator current;
public void startAnim() {
if (current != null) {
current.cancel();
}
current = ObjectAnimator.ofFloat(Test.this, "drag", 0.0f, 1.0f).setDuration(mDuration);
current.start();
}
private void scalePath(Path path, float scale) {
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(scale, scale);
path.transform(scaleMatrix);
}
private void init() {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(8.0f);
paint.setColor(0xffffffff);
mDuration = 3000;
Path p1 = makeDragPath(40);
scalePath(p1, 3);
path1 = new MyPath(p1, paint);
startAnim();
}
public float getDrag() {
return mDrag;
}
public void setDrag(float drag) {
mDrag = drag;
path1.paint.setPathEffect(createPathEffect2(path1.length, mDrag));
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.BLACK); //doesn't help
canvas.drawPath(path1.path, path1.paint);
}
}
In my activity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
final Test test = (Test)findViewById(R.id.test);
Button button = (Button)findViewById(R.id.startTest);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
test.startAnim();
}
});
}
xml:
<Button
android:id="#+id/startTest"
android:layout_width="200dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_height="60dp" />
<com.example.ivanschuetzd.myapplication.Test
android:id="#+id/test"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:background="#000000"
/>
Any idea? Or maybe a different approach to make this kind of animation with an arbitrary path? Thanks!

Just found a solution for that.
Apparently the problem is the hardware acceleration that is somewhat buggy with the DashPathEffect.
Just call
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
in your view and that should fix it.

Related

Oval path Animation using image

I am trying to implement Oval path animation, I want to show path animation using image, I tried https://github.com/matthewrkula/AnimatedPathView but it's not work for oval. I also tried below code for oval path but it is shows circle, Anyone have an idea? Thanks in advance!!!
MyAnimation.java
public class MyAnimation extends Animation {
private View view;
private float cx, cy; // center x,y position of circular path
private float prevX, prevY; // previous x,y position of image during animation
private float r; // radius of circle
private float prevDx, prevDy;
/**
* #param view - View that will be animated
* #param r - radius of circular path
*/
public MyAnimation(View view, float r){
this.view = view;
this.r = r;
}
#Override
public boolean willChangeBounds() {
return true;
}
#Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
// calculate position of image center
int cxImage = width / 2;
int cyImage = height / 1;
cx = view.getLeft() + cxImage;
cy = view.getTop() + cyImage;
// set previous position to center
prevX = cx;
prevY = cy;
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if(interpolatedTime == 0){
t.getMatrix().setTranslate(prevDx, prevDy);
return;
}
float angleDeg = (interpolatedTime * 360f + 90) % 360;
float angleRad = (float) Math.toRadians(angleDeg);
// r = radius, cx and cy = center point, a = angle (radians)
float x = (float) (cx + r * Math.cos(angleRad));
float y = (float) (cy + r * Math.sin(angleRad));
float dx = prevX - x;
float dy = prevY - y;
prevX = x;
prevY = y;
prevDx = dx;
prevDy = dy;
t.getMatrix().setTranslate(dx, dy);
}
}
PathAnimation.java
image = (ImageView) findViewById(R.id.image);
image.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Animation anim = new MyAnimation(image, 300);
anim.setDuration(1000);
image.startAnimation(anim);
}
});
I have found the solution after many tried using this custom class
AnimationView.java
public class AnimationView extends View {
Paint paint;
long animationDuration = 10000;
int framesPerSecond = 60;
Bitmap bm;
int bm_offsetX, bm_offsetY;
Path animPath;
PathMeasure pathMeasure;
float pathLength;
float step; //distance each step
float distance; //distance moved
float[] pos;
float[] tan;
Matrix matrix;
public AnimationView(Context context) {
super(context);
initMyView();
}
public AnimationView(Context context, AttributeSet attrs) {
super(context, attrs);
initMyView();
}
public AnimationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initMyView();
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void initMyView(){
paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);
bm = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
bm_offsetX = bm.getWidth()/2;
bm_offsetY = bm.getHeight()/2;
animPath = new Path();
animPath.moveTo(100, 100);
animPath.addArc(new RectF(1, 100, 300, 600), 1, 800);
animPath.close();
pathMeasure = new PathMeasure(animPath, false);
pathLength = pathMeasure.getLength();
Toast.makeText(getContext(), "pathLength: " + pathLength, Toast.LENGTH_LONG).show();
step = 1;
distance = 0;
pos = new float[2];
tan = new float[2];
matrix = new Matrix();
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawPath(animPath, paint);
if(distance < pathLength){
pathMeasure.getPosTan(distance, pos, tan);
matrix.reset();
float degrees = (float)(Math.atan2(tan[1], tan[0])*180.0/Math.PI);
matrix.postRotate(degrees, bm_offsetX, bm_offsetY);
matrix.postTranslate(pos[0]-bm_offsetX, pos[1]-bm_offsetY);
canvas.drawBitmap(bm, matrix, null);
distance += step;
}else{
distance = 0;
}
invalidate();
}
}
and put into xml
<com.example.android.mydemo.animation.pathanimation.AnimationView
android:layout_width="match_parent"
android:layout_height="450dp" />

Custom drawable with ImageView

I have already an 'ImageView' with these parameters:
android:layout_width="wrap_content"
android:layout_height="wrap_content"
and set custom Drawable :
public class HexDrawable extends Drawable {
private Path hexagonPath;
private float mWidth, mHeight;
private int mBackgroundColor;
private int mStrokeColor;
private int mStrokeWidth;
public HexDrawable(){
init();
}
public void setBackgroundColor(int color) {
mBackgroundColor = color;
}
public void setStrokeWidth(int width) {
mStrokeWidth = width;
}
public void setStrokeColor(int color) {
mStrokeColor = color;
}
#Override
public int getIntrinsicHeight() {
return 60;
}
#Override
public int getIntrinsicWidth() {
return 60;
}
private void init() {
hexagonPath = new Path();
mBackgroundColor = Color.BLUE;
mStrokeColor = Color.GREEN;
mStrokeWidth = 4;
}
private void calculatePath() {
float p = mStrokeWidth / 2;
float w = mWidth - 2 * p;
float h = mHeight - 2 * p;
float r = h / 2;
float a = (float) (r / Math.sqrt(3));
PointF X = new PointF(p + a + r / 2, p);
PointF Y = new PointF(p + a + r , p);
PointF A = new PointF(p + a, p + 0f);
PointF B = new PointF(p + 0f, p + r);
PointF C = new PointF(p + a, p + 2 * r);
PointF D = new PointF(p + w - a, p + 2 * r);
PointF E = new PointF(p + w, p + r);
PointF F = new PointF(p + w - a, p + 0);
hexagonPath.moveTo(Y.x, Y.y);
hexagonPath.lineTo(A.x, A.y);
hexagonPath.lineTo(B.x, B.y);
hexagonPath.lineTo(C.x, C.y);
hexagonPath.lineTo(D.x, D.y);
hexagonPath.lineTo(E.x, E.y);
hexagonPath.lineTo(F.x, F.y);
hexagonPath.lineTo(X.x, X.y);
}
#Override
protected void onBoundsChange(Rect bounds) {
mWidth = bounds.width();
mHeight = bounds.height();
calculatePath();
}
#Override
public void draw(Canvas canvas) {
Paint paint = new Paint();
paint.setColor(mStrokeColor); // set the color
paint.setStrokeWidth(mStrokeWidth); // set the size
paint.setDither(true); // set the dither to true
paint.setStyle(Paint.Style.STROKE); // set to STOKE
paint.setStrokeJoin(Paint.Join.ROUND); // set the join to round you want
paint.setStrokeCap(Paint.Cap.ROUND); // set the paint cap to round too
paint.setPathEffect(new CornerPathEffect(mStrokeWidth)); // set the path effect when they join.
paint.setAntiAlias(true);
canvas.drawPath(hexagonPath, paint);
canvas.clipPath(hexagonPath, Region.Op.INTERSECT);
canvas.drawColor(mBackgroundColor);
canvas.drawPath(hexagonPath, paint);
canvas.save();
}
#Override
public void setAlpha(int alpha) {
}
#Override
public void setColorFilter(ColorFilter colorFilter) {
}
#Override
public int getOpacity() {
return 0;
}
}
It seems that ImageView use all width in this case.
How to implements Drawable correctly to use it with ImageView?
The root of the problem was clip mode.
Its better to use canvas.clipPath(hexagonPath, Region.Op.REPLACE);
Also, question example works well with ImageView, but after deep investigation I undestand, that at android 5.0 and above this drawable is used at drawableLeft in TextView.
Also its not need to override getIntrinsicHeight
Your code is absolutely correct:
This is how I fill ImageView (in onCreate() of activity):
((ImageView)findViewById(R.id.hexImageView)).setImageDrawable(new HexDrawable());
Layout of the Activity on the screenshot:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<ImageView
android:id="#+id/hexImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
If I replace wrap_content with value, hexagon as expected is changing its size.
Tested on Android 6.0 and 4.3.
Just one tip - instead of hardcoded values in getIntrinsicHeight() and getIntrinsicWidth(), it might be better to use dimens instead.

Customize a ProgressBar to become a Thermometer

How to customize a ProgressBar to look like a Thermometer ? with the possibility to change color.
My suggestion was to rotate the progressBar 90° to become vertical then have it overlay an image of an empty Thermometer but it's bad and messy solution.
I Think the best will be to either to extends View or ProgressBar class and customize the draw method but I have no idea how to draw Thermometer, any Help would be appreciated.
I created something like this for a project
package com.janslab.thermometer.widgets;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.Scroller;
import com.janslab.thermometer.R;
public class DummyThermometer extends View {
private Paint mInnerCirclePaint;
private Paint mOuterCirclePaint;
private Paint mFirstOuterCirclePaint;
//thermometer arc paint
private Paint mFirstOuterArcPaint;
//thermometer lines paints
private Paint mInnerLinePaint;
private Paint mOuterLinePaint;
private Paint mFirstOuterLinePaint;
//thermometer radii
private int mOuterRadius;
private int mInnerRadius;
private int mFirstOuterRadius;
//thermometer colors
private int mThermometerColor = Color.rgb(200, 115, 205);
//circles and lines variables
private float mLastCellWidth;
private int mStageHeight;
private float mCellWidth;
private float mStartCenterY; //center of first cell
private float mEndCenterY; //center of last cell
private float mStageCenterX;
private float mXOffset;
private float mYOffset;
// I 1st Cell I 2nd Cell I 3rd Cell I
private static final int NUMBER_OF_CELLS = 3; //three cells in all ie.stageHeight divided into 3 equal cells
//animation variables
private float mIncrementalTempValue;
private boolean mIsAnimating;
private Animator mAnimator;
public DummyThermometer(Context context) {
this(context, null);
}
public DummyThermometer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DummyThermometer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if (attrs != null) {
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Thermometer, defStyle, 0);
mThermometerColor = a.getColor(R.styleable.Thermometer_therm_color, mThermometerColor);
a.recycle();
}
init();
}
private void init() {
mInnerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mInnerCirclePaint.setColor(mThermometerColor);
mInnerCirclePaint.setStyle(Paint.Style.FILL);
mInnerCirclePaint.setStrokeWidth(17f);
mOuterCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mOuterCirclePaint.setColor(Color.WHITE);
mOuterCirclePaint.setStyle(Paint.Style.FILL);
mOuterCirclePaint.setStrokeWidth(32f);
mFirstOuterCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFirstOuterCirclePaint.setColor(mThermometerColor);
mFirstOuterCirclePaint.setStyle(Paint.Style.FILL);
mFirstOuterCirclePaint.setStrokeWidth(60f);
mFirstOuterArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFirstOuterArcPaint.setColor(mThermometerColor);
mFirstOuterArcPaint.setStyle(Paint.Style.STROKE);
mFirstOuterArcPaint.setStrokeWidth(30f);
mInnerLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mInnerLinePaint.setColor(mThermometerColor);
mInnerLinePaint.setStyle(Paint.Style.FILL);
mInnerLinePaint.setStrokeWidth(17f);
mOuterLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mOuterLinePaint.setColor(Color.WHITE);
mOuterLinePaint.setStyle(Paint.Style.FILL);
mFirstOuterLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFirstOuterLinePaint.setColor(mThermometerColor);
mFirstOuterLinePaint.setStyle(Paint.Style.FILL);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mStageCenterX = getWidth() / 2;
mStageHeight = getHeight();
mCellWidth = mStageHeight / NUMBER_OF_CELLS;
//center of first cell
mStartCenterY = mCellWidth / 2;
//move to 3rd cell
mLastCellWidth = (NUMBER_OF_CELLS * mCellWidth);
//center of last(3rd) cell
mEndCenterY = mLastCellWidth - (mCellWidth / 2);
// mOuterRadius is 1/4 of mCellWidth
mOuterRadius = (int) (0.25 * mCellWidth);
mInnerRadius = (int) (0.656 * mOuterRadius);
mFirstOuterRadius = (int) (1.344 * mOuterRadius);
mFirstOuterLinePaint.setStrokeWidth(mFirstOuterRadius);
mOuterLinePaint.setStrokeWidth(mFirstOuterRadius / 2);
mFirstOuterArcPaint.setStrokeWidth(mFirstOuterRadius / 4);
mXOffset = mFirstOuterRadius / 4;
mXOffset = mXOffset / 2;
//get the d/f btn firstOuterLine and innerAnimatedline
mYOffset = (mStartCenterY + (float) 0.875 * mOuterRadius) - (mStartCenterY + mInnerRadius);
mYOffset = mYOffset / 2;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawFirstOuterCircle(canvas);
drawOuterCircle(canvas);
drawInnerCircle(canvas);
drawFirstOuterLine(canvas);
drawOuterLine(canvas);
animateInnerLine(canvas);
drawFirstOuterCornerArc(canvas);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//take care of paddingTop and paddingBottom
int paddingY = getPaddingBottom() + getPaddingTop();
//get height and width
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
height += paddingY;
setMeasuredDimension(width, height);
}
private void drawInnerCircle(Canvas canvas) {
drawCircle(canvas, mInnerRadius, mInnerCirclePaint);
}
private void drawOuterCircle(Canvas canvas) {
drawCircle(canvas, mOuterRadius, mOuterCirclePaint);
}
private void drawFirstOuterCircle(Canvas canvas) {
drawCircle(canvas, mFirstOuterRadius, mFirstOuterCirclePaint);
}
private void drawCircle(Canvas canvas, float radius, Paint paint) {
canvas.drawCircle(mStageCenterX, mEndCenterY, radius, paint);
}
private void drawOuterLine(Canvas canvas) {
float startY = mEndCenterY - (float) (0.875 * mOuterRadius);
float stopY = mStartCenterY + (float) (0.875 * mOuterRadius);
drawLine(canvas, startY, stopY, mOuterLinePaint);
}
private void drawFirstOuterLine(Canvas canvas) {
float startY = mEndCenterY - (float) (0.875 * mFirstOuterRadius);
float stopY = mStartCenterY + (float) (0.875 * mOuterRadius);
drawLine(canvas, startY, stopY, mFirstOuterLinePaint);
}
private void drawLine(Canvas canvas, float startY, float stopY, Paint paint) {
canvas.drawLine(mStageCenterX, startY, mStageCenterX, stopY, paint);
}
//simulate temperature measurement for now
private void animateInnerLine(Canvas canvas) {
if (mAnimator == null)
measureTemperature();
if (!mIsAnimating) {
mIncrementalTempValue = mEndCenterY + (float) (0.875 * mInnerRadius);
mIsAnimating = true;
} else {
mIncrementalTempValue = mEndCenterY + (float) (0.875 * mInnerRadius) - mIncrementalTempValue;
}
if (mIncrementalTempValue > mStartCenterY + mInnerRadius) {
float startY = mEndCenterY + (float) (0.875 * mInnerRadius);
drawLine(canvas, startY, mIncrementalTempValue, mInnerCirclePaint);
} else {
float startY = mEndCenterY + (float) (0.875 * mInnerRadius);
float stopY = mStartCenterY + mInnerRadius;
drawLine(canvas, startY, stopY, mInnerCirclePaint);
mIsAnimating = false;
stopMeasurement();
}
}
private void drawFirstOuterCornerArc(Canvas canvas) {
float y = mStartCenterY - (float) (0.875 * mFirstOuterRadius);
RectF rectF = new RectF(mStageCenterX - mFirstOuterRadius / 2 + mXOffset, y + mFirstOuterRadius, mStageCenterX + mFirstOuterRadius / 2 - mXOffset, y + (2 * mFirstOuterRadius) + mYOffset);
canvas.drawArc(rectF, -180, 180, false, mFirstOuterArcPaint);
}
public void setThermometerColor(int thermometerColor) {
this.mThermometerColor = thermometerColor;
mInnerCirclePaint.setColor(mThermometerColor);
mFirstOuterCirclePaint.setColor(mThermometerColor);
mFirstOuterArcPaint.setColor(mThermometerColor);
mInnerLinePaint.setColor(mThermometerColor);
mFirstOuterLinePaint.setColor(mThermometerColor);
invalidate();
}
//simulate temperature measurement for now
private void measureTemperature() {
mAnimator = new Animator();
mAnimator.start();
}
private class Animator implements Runnable {
private Scroller mScroller;
private final static int ANIM_START_DELAY = 1000;
private final static int ANIM_DURATION = 4000;
private boolean mRestartAnimation = false;
public Animator() {
mScroller = new Scroller(getContext(), new AccelerateDecelerateInterpolator());
}
public void run() {
if (mAnimator != this)
return;
if (mRestartAnimation) {
int startY = (int) (mStartCenterY - (float) (0.875 * mInnerRadius));
int dy = (int) (mEndCenterY + mInnerRadius);
mScroller.startScroll(0, startY, 0, dy, ANIM_DURATION);
mRestartAnimation = false;
}
boolean isScrolling = mScroller.computeScrollOffset();
mIncrementalTempValue = mScroller.getCurrY();
if (isScrolling) {
invalidate();
post(this);
} else {
stop();
}
}
public void start() {
mRestartAnimation = true;
postDelayed(this, ANIM_START_DELAY);
}
public void stop() {
removeCallbacks(this);
mAnimator = null;
}
}
private void stopMeasurement() {
if (mAnimator != null)
mAnimator.stop();
}
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
measureTemperature();
}
#Override
protected void onDetachedFromWindow() {
stopMeasurement();
super.onDetachedFromWindow();
}
#Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
switch (visibility) {
case View.VISIBLE:
measureTemperature();
break;
default:
stopMeasurement();
break;
}
}
}
attrs.xml file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="Thermometer">
<attr name="therm_color" format="color" />
</declare-styleable>
</resources>
First I would provide 2 setters, one for color and one for the temperature value, normalized from 0 ... 1, where 0 means no visible bar, and 1 means a fully visible bar.
public void setColor(int color) {
mColor = color;
invalidate(); // important, this triggers onDraw
}
public void setValue(float value) {
mValue = -(value - 1);
invalidate(); // important, this triggers onDraw
}
Notice for value, I reverse the value, since we draw the bar from bottom up, instead from top down. It makes sense in the canvas.drawRect method.
If your CustomView may have custom sizes, set your size of the progressBar (I refer to the inner bar as progressBar) in onSizeChanged, as this gets called when the View has changed it's size.
If it is a fixed size, you can just provide those values statically in an init function or the constructor.
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mProgressRect = new Rect(
/*your bar left offset relative to base bitmap*/,
/*your bar top offset relative to base bitmap*/,
/*your bar total width*/,
/*your max bar height*/
);
}
Then in ondraw, take these values into account and draw accordingly.
First draw the Bitmap, depending on your selected color (I would provide the thermometer base as a Bitmap, as long as it does not have to be completely dynamically drawn (special requirements)
Then draw the progress bar, with an height based on mValue * totalHeight of the bar, using the color provided in the setter.
For example:
#Override
protected void onDraw(Canvas canvas) {
// draw your thermometer base, bitmap based on color value
canvas.drawBitmap( /*your base thermometer bitmap here*/ );
// draw the "progress"
canvas.drawRect(mProgressRect.left, mProgressRect.top + (mValue * mProgressRect.bottom - mProgressRect.top), mProgressRect.right, mProgressRect.bottom, mPaint);
}
Hope that helps.
P.S.:
If you want to have the thermometer base image also dynamically drawn, it's a slightly different story, it would involve creating a path first and draw it with a Paint object, instead of drawing the bitmap.
EDIT:
Even better, if you want a simple solution for the "roundness" of the bar, draw a line instead a rect.
Define a line paint object like this:
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(20); // thickness of your bar
then in onDraw, instead drawRect:
// draw the "progress"
canvas.drawLine(mProgressRect.left, mProgressRect.top + (mValue * mProgressRect.bottom - mProgressRect.top), mProgressRect.left, mProgressRect.bottom, mPaint);
Be sure to adjust your mProgressRectaccordingly.

android : Xfermode masking disappearing on 180 degree rotation of Canvas

I am inspired by the the new Material Design animations and I worked to create a similar drawable that is used in new support v7 Action Bar Drawer Toggle.
I created a CustomDrawable. All I actually did is that I created a Play triangle on canvas and pause logo on the left of the left margin of the visible canvas. I rotate the canvas according to the progress and restore it. Then I used Xfermode to crop the rotated result into a circle.
I cant find the solution to the problem.
The problem is that the xFermode is not applied to the 180 degree rotated result(after calling canvas.restore()).
Here is the code of Activity.
public class MainActivity extends Activity{
ImageView iv;
CustomDrawable drawable = new CustomDrawable();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
iv = (ImageView) findViewById(R.id.button);
iv.setLayerType(View.LAYER_TYPE_HARDWARE, null);
iv.setBackgroundDrawable(drawable);
iv.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
float[] values = { 0, 1 };
if (drawable.getProgress() != 0) {
values[0] = drawable.getProgress();
values[1] = 0;
}
ObjectAnimator animator = ObjectAnimator.ofFloat(drawable,
"progress", values);
animator.setDuration(2000);
animator.start();
}
});
}
}
And the code for the CustomDrawable
public class CustomDrawable extends Drawable {
private float mProgress = 0;
private Paint mPaint = new Paint();
private Path mPath = new Path();
private final float rootTwo = (float) Math.sqrt(2);
private final float rootThree = (float) Math.sqrt(3);
private float radius = 0;
private float side = 0;
private Point[] triangle = new Point[3];
Paint xferpaint = new Paint();
Canvas cropper;
Bitmap bitmap;
Interpolator interpolator = new AnticipateOvershootInterpolator();
private float width;
Rect rec1, rec2;
public CustomDrawable() {
mPaint.setAntiAlias(true);
mPaint.setStyle(Style.FILL);
xferpaint.setColor(Color.RED);
xferpaint.setStyle(Style.FILL);
xferpaint.setAntiAlias(true);
}
#Override
public void draw(Canvas canvas) {
canvas.getClipBounds(bound);
boundsf.set(bound);
if (radius == 0) {
radius = Math.min(bound.centerX(), bound.centerY());
radius -= 5;
bitmap = Bitmap.createBitmap(bound.width(), bound.height(),
Config.ARGB_8888);
cropper = new Canvas(bitmap);
cropper.drawCircle(bound.centerX(), bound.centerY(), radius,
xferpaint);
xferpaint.setXfermode(new PorterDuffXfermode(Mode.DST_IN));
side = rootTwo * radius;
triangle[0] = new Point(
(int) (bound.centerX() + (side / rootThree)),
bound.centerY());
triangle[1] = new Point(bound.centerX()
- (int) (side / (2 * rootThree)), bound.centerY()
- (int) (side / 2));
triangle[2] = new Point(bound.centerX()
- (int) (side / (2 * rootThree)), bound.centerY()
+ (int) (side / 2));
width = side / 4;
rec1 = new Rect((int) (-bound.centerX() - (3 * width / 2)),
(int) (bound.centerY() - (side / 2)),
(int) (-bound.centerX() - (width / 2)),
(int) (bound.centerY() + (side / 2)));
rec2 = new Rect((int) (-bound.centerX() + (width / 2)),
(int) (bound.centerY() - (side / 2)),
(int) (-bound.centerX() + (3 * width / 2)),
(int) (bound.centerY() + (side / 2)));
}
mPath.rewind();
mPath.moveTo(triangle[0].x, triangle[0].y);
mPath.lineTo(triangle[1].x, triangle[1].y);
mPath.lineTo(triangle[2].x, triangle[2].y);
mPath.close();
mPaint.setColor(Color.parseColor("#378585"));
canvas.drawPaint(mPaint);
mPaint.setColor(Color.parseColor("#FF0400"));
canvas.rotate(180 * interpolator.getInterpolation(mProgress), 0,
bound.centerY());
canvas.drawPath(mPath, mPaint);
canvas.drawRect(rec1, mPaint);
canvas.drawRect(rec2, mPaint);
canvas.restore();
canvas.drawBitmap(bitmap, 0, 0, xferpaint);
}
#Override
public int getOpacity() {
return mPaint.getAlpha();
}
#Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
#Override
public void setColorFilter(ColorFilter filter) {
mPaint.setColorFilter(filter);
}
public float getProgress() {
return mProgress;
}
public void setProgress(float progress) {
mProgress = progress;
invalidateSelf();
}
}
this is your simplified Drawable:
class CustomDrawable extends Drawable implements ValueAnimator.AnimatorUpdateListener {
private float mProgress = 0;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Path mPath;
private Path mClipPath;
public CustomDrawable() {
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(0xffFF0400);
}
#Override
protected void onBoundsChange(Rect bounds) {
mClipPath = new Path();
int cx = bounds.centerX();
int cy = bounds.centerY();
int radius = Math.min(cx, cy) - 5;
mClipPath.addCircle(cx, cy, radius, Path.Direction.CCW);
final float rootTwo = (float) Math.sqrt(2);
final float rootThree = (float) Math.sqrt(3);
float side = rootTwo * radius;
mPath = new Path();
mPath.moveTo(cx + (side / rootThree), cy);
mPath.lineTo(cx - side / (2 * rootThree), cy - side / 2);
mPath.lineTo(cx - side / (2 * rootThree), cy + side / 2);
mPath.close();
float width = side / 4;
addRect(-cx - (3 * width / 2), cy - (side / 2), width, side);
addRect(-cx + (width / 2), cy - (side / 2), width, side);
}
private void addRect(float l, float t, float dx, float dy) {
mPath.addRect(l, t, l + dx, t + dy, Path.Direction.CCW);
}
#Override
public void draw(Canvas canvas) {
canvas.clipPath(mClipPath);
canvas.drawColor(0xff378585);
Rect bounds = getBounds();
canvas.rotate(mProgress, 0, bounds.centerY());
canvas.drawPath(mPath, mPaint);
}
public void switchIcons() {
float[] values = { 0, 180 };
if (mProgress != 0) {
values[0] = mProgress;
values[1] = 0;
}
ValueAnimator animator = ValueAnimator.ofFloat(values).setDuration(2000);
animator.setInterpolator(new AnticipateOvershootInterpolator());
animator.addUpdateListener(this);
animator.start();
}
#Override
public void onAnimationUpdate(ValueAnimator animation) {
mProgress = (Float) animation.getAnimatedValue();
invalidateSelf();
}
#Override
public int getOpacity() {
return mPaint.getAlpha();
}
#Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
#Override
public void setColorFilter(ColorFilter filter) {
mPaint.setColorFilter(filter);
}
}
EDIT: this is draw() method without Canvas.clipPath and without creating a "mask" Bitmap:
#Override
public void draw(Canvas canvas) {
canvas.drawColor(0xff378585);
canvas.save();
canvas.rotate(mProgress, 0, cy);
canvas.drawPath(mPath, mPaint);
canvas.restore();
canvas.saveLayer(null, mDstInPaint, 0);
canvas.drawCircle(cx, cy, radius, mPaint);
canvas.restore();
}
where: mDstInPaint is a Paint object with xfermode set in Drawable's constructor:
mDstInPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));

How to set ontouch listener for something drawn using canvas: Android

I have a custom view in which i am drawing one big circle and a small circle on the edge of this big circle.
I would like to move the small circle and so would like to have a ontouch listener only for the small circle.
Could some please tell me how to set the ontouch listener for only the small circle.
public class ThermoView extends View{
private ImageView mThermostatBgrd;
private ImageView mCurTempArrow;
private ImageView mSetPointIndicator;
public static final int THEMROSTAT_BACKGROUND = 0;
public static final int THEMROSTAT_CURR_TEMP = 1;
public static final int THEMROSTAT_SET_POINT = 2;
private float mViewCentreX;
private float mViewCentreY;
private float mThermostatRadius;
private Canvas mCanvas;
private Paint mPaint;
private Paint mPaintCurTemp;
private Paint mPaintSetTemp;
private Paint mPaintOverrideTemp;
private Paint mPaintCurTempIndicator;
private Boolean mManualOverride = false;
private double mManualOverrideAngle;
private int mMaxTemp = 420;
private int mMinTemp = 120;
private RectF mCurrTempBox;
private float mCurTempCircleX;
private float mCurTempCircleY;
private Matrix mMatrix;
public double getManualOverrideAngle() {
return mManualOverrideAngle;
}
public void setManualOverrideAngle(double mManualOverrideAngle) {
this.mManualOverrideAngle = mManualOverrideAngle;
}
public RectF getCurrTempBox() {
if (mCurrTempBox == null){
mCurrTempBox = new RectF();
}
return mCurrTempBox;
}
public void setCurrTempBox(RectF mCurrTempBox) {
this.mCurrTempBox = mCurrTempBox;
}
public Boolean getManualOverride() {
return mManualOverride;
}
public void setManualOverride(Boolean mManualOverride) {
this.mManualOverride = mManualOverride;
}
public ThermoView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Path smallCirle = new Path();
int viewWidth = getMeasuredWidth();
int viewHeight = getMeasuredHeight();
mViewCentreX = viewWidth/2;
mViewCentreY = viewHeight/2;
float paddingPercent = 0.2f;
int thermostatThickness = 20;
mThermostatRadius = (int) ((Math.min(mViewCentreX, mViewCentreY)*(1- paddingPercent)));
if (mPaint == null){
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(thermostatThickness);
mPaint.setColor(0xffff0000);
Path arcPath = new Path();
RectF container = new RectF();
container.set(mViewCentreX - mThermostatRadius, mViewCentreY - mThermostatRadius,
mViewCentreX + mThermostatRadius, mViewCentreY + mThermostatRadius);
arcPath.addArc(container, 120, 300);
canvas.drawPath(arcPath, mPaint);
int dummyCurTemp = 200;
if (mPaintCurTemp == null){
mPaintCurTemp = new Paint(Paint.ANTI_ALIAS_FLAG);
}
mPaintCurTemp.setTextAlign(Align.CENTER);
mPaintCurTemp.setTextSize(100);
canvas.drawText(String.valueOf(dummyCurTemp), mViewCentreX, mViewCentreY, mPaintCurTemp);
if (this.mManualOverride == false){
double angle = (360-120-(300/(mMaxTemp - mMinTemp))*(dummyCurTemp-mMinTemp))*(Math.PI/180);
this.mCurTempCircleX = (float) (mViewCentreX + mThermostatRadius*(Math.cos(angle)));
this.mCurTempCircleY = (float) (mViewCentreY - mThermostatRadius*(Math.sin(angle)));
if (mCurrTempBox == null){
mCurrTempBox = new RectF();
}
if (mPaintCurTempIndicator == null){
mPaintCurTempIndicator = new Paint(Paint.ANTI_ALIAS_FLAG);
}
mPaintCurTempIndicator.setStyle(Paint.Style.STROKE);
mPaintCurTempIndicator.setStrokeWidth(thermostatThickness/2);
mPaintCurTempIndicator.setColor(Color.GREEN);
mCurrTempBox.set(mCurTempCircleX-50, mCurTempCircleY-50, mCurTempCircleX+50, mCurTempCircleY+50);
smallCirle.addCircle(mCurTempCircleX, mCurTempCircleY, 50, Direction.CW);
canvas.drawPath(smallCirle, mPaintCurTempIndicator);
}else{
if (mCurrTempBox == null){
mCurrTempBox = new RectF();
}
if (mPaintCurTempIndicator == null){
mPaintCurTempIndicator = new Paint(Paint.ANTI_ALIAS_FLAG);
}
if (mMatrix == null){
mMatrix = new Matrix();
}
//mMatrix.reset();
mMatrix.postRotate((float) (mManualOverrideAngle), mViewCentreX,mViewCentreY);
//mMatrix.postTranslate(mViewCentreX, mViewCentreY);
mPaintCurTempIndicator.setStyle(Paint.Style.STROKE);
mPaintCurTempIndicator.setStrokeWidth(thermostatThickness/2);
mPaintCurTempIndicator.setColor(Color.GREEN);
canvas.concat(mMatrix);
smallCirle.addCircle(mCurTempCircleX, mCurTempCircleY, 50, Direction.CW);
canvas.drawPath(smallCirle, mPaintCurTempIndicator);
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mInitialX = event.getX();
mInitialY = event.getY();
RectF touchedAt = new RectF(mInitialX-10, mInitialY-10, mInitialX+10, mInitialY+10);
RectF indicatorAt = mThermoStatView.getCurrTempBox();
if (RectF.intersects(indicatorAt, touchedAt)){
this.isIndicatorSelected = true;
mThermoStatView.setManualOverride(true);
}
break;
case MotionEvent.ACTION_MOVE:
if (this.isIndicatorSelected == true){
float angle = (float) (180*Math.atan2(event.getY() - mThermostatHeight/2, event.getX() - mThermostatWidth/2) / Math.PI);
mThermoStatView.setManualOverrideAngle(angle);
mThermoStatView.invalidate();
//mThermoStatView.requestLayout();
}
break;
case MotionEvent.ACTION_UP:
if (this.isIndicatorSelected == true){
this.isIndicatorSelected = false;
}
break;
}
return true;
}
}
try this (this is a little modified version of MyView i already posted as an answer for your previous question):
public class MyView extends View {
private final static String TAG = "Main.MyView";
private static final float CX = 0;
private static final float CY = 0;
private static final float RADIUS = 20;
private static final float BIGRADIUS = 50;
private static final int NORMAL_COLOR = 0xffffffff;
private static final int PRESSED_COLOR = 0xffff0000;
private Paint mPaint;
private Path mSmallCircle;
private Path mCircle;
private Matrix mMatrix;
private float mAngle;
private int mSmallCircleColor;
public MyView(Context context) {
super(context);
mPaint = new Paint();
mSmallCircle = new Path();
mSmallCircle.addCircle(BIGRADIUS + RADIUS + CX, CY, RADIUS, Direction.CW);
mSmallCircleColor = NORMAL_COLOR;
mCircle = new Path();
mCircle.addCircle(0, 0, BIGRADIUS, Direction.CW);
mMatrix = new Matrix();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP) {
mSmallCircleColor = NORMAL_COLOR;
invalidate();
return false;
}
float w2 = getWidth() / 2f;
float h2 = getHeight() / 2f;
float r = 0;
if (action == MotionEvent.ACTION_DOWN) {
float[] pts = {
BIGRADIUS + RADIUS + CX, CY
};
mMatrix.mapPoints(pts);
r = (float) Math.hypot(event.getX() - pts[0], event.getY() - pts[1]);
}
if (r < RADIUS) {
mSmallCircleColor = PRESSED_COLOR;
mAngle = (float) (180 * Math.atan2(event.getY() - h2, event.getX() - w2) / Math.PI);
invalidate();
return true;
}
return false;
}
#Override
protected void onDraw(Canvas canvas) {
float w2 = getWidth() / 2f;
float h2 = getHeight() / 2f;
mMatrix.reset();
mMatrix.postRotate(mAngle);
mMatrix.postTranslate(w2, h2);
canvas.concat(mMatrix);
mPaint.setColor(0x88ffffff);
canvas.drawPath(mCircle, mPaint);
mPaint.setColor(mSmallCircleColor);
canvas.drawPath(mSmallCircle, mPaint);
}
}
You can get the rectangle of your touch point and the rectangle(position) of your inner circle and check if they cross over with the Intersects method.
http://docs.oracle.com/javase/7/docs/api/java/awt/Rectangle.html#intersects(java.awt.Rectangle)
Your canvas onTouchListener can do whatever it needs to do if the touchpoint intersect your circle.
e.g:
// Create a rectangle from the point of touch
Rect touchpoint = new Rect(x,y,10,10);
// Create a rectangle from the postion of the circle.
Rect myCircle=new Rect(10,10,20,20);
if (Rect.intersects(myCircle,touchpoint)){
Log.d("The circle was touched");
}

Categories

Resources