Related
hey guys I am working on a launcher that's based off the rootless pixel launcher and am trying to change the folder icon to a squircle shape rather than a oval/circle shape. It looks like the launcher is making the folder icon using canvas draw method but I have little experience with this approach usually i'd make a xml shape in reference the shape that way, but when doing research on this it looks like you can not make a squircle xml shape so with that being said here is the class that makes the folder icon:
/*
* Copyright (C) 2017 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.android.launcher3.folder;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RadialGradient;
import android.graphics.Region;
import android.graphics.Shader;
import android.support.v4.graphics.ColorUtils;
import android.util.Property;
import android.view.View;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.util.Themes;
/**
* This object represents a FolderIcon preview background. It stores drawing / measurement
* information, handles drawing, and animation (accept state <--> rest state).
*/
public class PreviewBackground {
private static final int CONSUMPTION_ANIMATION_DURATION = 100;
private final PorterDuffXfermode mClipPorterDuffXfermode
= new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
// Create a RadialGradient such that it draws a black circle and then extends with
// transparent. To achieve this, we keep the gradient to black for the range [0, 1) and
// just at the edge quickly change it to transparent.
private final RadialGradient mClipShader = new RadialGradient(0, 0, 1,
new int[] {Color.BLACK, Color.BLACK, Color.TRANSPARENT },
new float[] {0, 0.999f, 1},
Shader.TileMode.CLAMP);
private final PorterDuffXfermode mShadowPorterDuffXfermode
= new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
private RadialGradient mShadowShader = null;
private final Matrix mShaderMatrix = new Matrix();
private final Path mPath = new Path();
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
float mScale = 1f;
private float mColorMultiplier = 1f;
private int mBgColor;
private float mStrokeWidth;
private int mStrokeAlpha = MAX_BG_OPACITY;
private int mShadowAlpha = 255;
private View mInvalidateDelegate;
int previewSize;
int basePreviewOffsetX;
int basePreviewOffsetY;
private CellLayout mDrawingDelegate;
public int delegateCellX;
public int delegateCellY;
// When the PreviewBackground is drawn under an icon (for creating a folder) the border
// should not occlude the icon
public boolean isClipping = true;
// Drawing / animation configurations
private static final float ACCEPT_SCALE_FACTOR = 1.20f;
private static final float ACCEPT_COLOR_MULTIPLIER = 1.5f;
// Expressed on a scale from 0 to 255.
private static final int BG_OPACITY = 160;
private static final int MAX_BG_OPACITY = 225;
private static final int SHADOW_OPACITY = 40;
private ValueAnimator mScaleAnimator;
private ObjectAnimator mStrokeAlphaAnimator;
private ObjectAnimator mShadowAnimator;
private static final Property<PreviewBackground, Integer> STROKE_ALPHA =
new Property<PreviewBackground, Integer>(Integer.class, "strokeAlpha") {
#Override
public Integer get(PreviewBackground previewBackground) {
return previewBackground.mStrokeAlpha;
}
#Override
public void set(PreviewBackground previewBackground, Integer alpha) {
previewBackground.mStrokeAlpha = alpha;
previewBackground.invalidate();
}
};
private static final Property<PreviewBackground, Integer> SHADOW_ALPHA =
new Property<PreviewBackground, Integer>(Integer.class, "shadowAlpha") {
#Override
public Integer get(PreviewBackground previewBackground) {
return previewBackground.mShadowAlpha;
}
#Override
public void set(PreviewBackground previewBackground, Integer alpha) {
previewBackground.mShadowAlpha = alpha;
previewBackground.invalidate();
}
};
public void setup(Launcher launcher, View invalidateDelegate,
int availableSpace, int topPadding) {
mInvalidateDelegate = invalidateDelegate;
mBgColor = Themes.getAttrColor(launcher, android.R.attr.colorPrimary);
DeviceProfile grid = launcher.getDeviceProfile();
final int previewSize = grid.folderIconSizePx;
final int previewPadding = grid.folderIconPreviewPadding;
this.previewSize = (previewSize - 2 * previewPadding);
basePreviewOffsetX = (availableSpace - this.previewSize) / 2;
basePreviewOffsetY = previewPadding + grid.folderBackgroundOffset + topPadding;
// Stroke width is 1dp
mStrokeWidth = launcher.getResources().getDisplayMetrics().density;
float radius = getScaledRadius();
float shadowRadius = radius + mStrokeWidth;
int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
mShadowShader = new RadialGradient(0, 0, 1,
new int[] {shadowColor, Color.TRANSPARENT},
new float[] {radius / shadowRadius, 1},
Shader.TileMode.CLAMP);
invalidate();
}
int getRadius() {
return previewSize / 2;
}
int getScaledRadius() {
return (int) (mScale * getRadius());
}
int getOffsetX() {
return basePreviewOffsetX - (getScaledRadius() - getRadius());
}
int getOffsetY() {
return basePreviewOffsetY - (getScaledRadius() - getRadius());
}
/**
* Returns the progress of the scale animation, where 0 means the scale is at 1f
* and 1 means the scale is at ACCEPT_SCALE_FACTOR.
*/
float getScaleProgress() {
return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
}
void invalidate() {
if (mInvalidateDelegate != null) {
mInvalidateDelegate.invalidate();
}
if (mDrawingDelegate != null) {
mDrawingDelegate.invalidate();
}
}
void setInvalidateDelegate(View invalidateDelegate) {
mInvalidateDelegate = invalidateDelegate;
invalidate();
}
public int getBgColor() {
int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
return ColorUtils.setAlphaComponent(mBgColor, alpha);
}
public void drawBackground(Canvas canvas) {
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(getBgColor());
//drawCircle(canvas, 0 /* deltaRadius */);
drawShadow(canvas);
}
public void drawShadow(Canvas canvas) {
if (mShadowShader == null) {
return;
}
float radius = getScaledRadius();
float shadowRadius = radius + mStrokeWidth;
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.BLACK);
int offsetX = getOffsetX();
int offsetY = getOffsetY();
final int saveCount;
if (canvas.isHardwareAccelerated()) {
saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY,
offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius,
null, Canvas.CLIP_TO_LAYER_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG);
} else {
saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.clipPath(getClipPath(), Region.Op.DIFFERENCE);
}
mShaderMatrix.setScale(shadowRadius, shadowRadius);
mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY);
mShadowShader.setLocalMatrix(mShaderMatrix);
mPaint.setAlpha(mShadowAlpha);
mPaint.setShader(mShadowShader);
canvas.drawPaint(mPaint);
mPaint.setAlpha(255);
mPaint.setShader(null);
if (canvas.isHardwareAccelerated()) {
mPaint.setXfermode(mShadowPorterDuffXfermode);
canvas.drawCircle(radius + offsetX, radius + offsetY, radius, mPaint);
mPaint.setXfermode(null);
}
canvas.restoreToCount(saveCount);
}
public void fadeInBackgroundShadow() {
if (mShadowAnimator != null) {
mShadowAnimator.cancel();
}
mShadowAnimator = ObjectAnimator
.ofInt(this, SHADOW_ALPHA, 0, 255)
.setDuration(100);
mShadowAnimator.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
mShadowAnimator = null;
}
});
mShadowAnimator.start();
}
public void animateBackgroundStroke() {
if (mStrokeAlphaAnimator != null) {
mStrokeAlphaAnimator.cancel();
}
mStrokeAlphaAnimator = ObjectAnimator
.ofInt(this, STROKE_ALPHA, MAX_BG_OPACITY / 2, MAX_BG_OPACITY)
.setDuration(100);
mStrokeAlphaAnimator.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
mStrokeAlphaAnimator = null;
}
});
mStrokeAlphaAnimator.start();
}
public void drawBackgroundStroke(Canvas canvas) {
mPaint.setColor(ColorUtils.setAlphaComponent(mBgColor, mStrokeAlpha));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mStrokeWidth);
drawCircle(canvas, 1 /* deltaRadius */);
}
public void drawLeaveBehind(Canvas canvas) {
float originalScale = mScale;
mScale = 0.5f;
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.argb(160, 245, 245, 245));
drawCircle(canvas, 0 /* deltaRadius */);
mScale = originalScale;
}
private void drawCircle(Canvas canvas,float deltaRadius) {
float radius = getScaledRadius();
canvas.drawCircle(radius + getOffsetX(), radius + getOffsetY(),
radius - deltaRadius, mPaint);
}
public Path getClipPath() {
mPath.reset();
float r = getScaledRadius();
mPath.addCircle(r + getOffsetX(), r + getOffsetY(), r, Path.Direction.CW);
return mPath;
}
// It is the callers responsibility to save and restore the canvas layers.
void clipCanvasHardware(Canvas canvas) {
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setXfermode(mClipPorterDuffXfermode);
float radius = getScaledRadius();
mShaderMatrix.setScale(radius, radius);
mShaderMatrix.postTranslate(radius + getOffsetX(), radius + getOffsetY());
mClipShader.setLocalMatrix(mShaderMatrix);
mPaint.setShader(mClipShader);
canvas.drawPaint(mPaint);
mPaint.setXfermode(null);
mPaint.setShader(null);
}
private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
if (mDrawingDelegate != delegate) {
delegate.addFolderBackground(this);
}
mDrawingDelegate = delegate;
delegateCellX = cellX;
delegateCellY = cellY;
invalidate();
}
private void clearDrawingDelegate() {
if (mDrawingDelegate != null) {
mDrawingDelegate.removeFolderBackground(this);
}
mDrawingDelegate = null;
isClipping = true;
invalidate();
}
boolean drawingDelegated() {
return mDrawingDelegate != null;
}
private void animateScale(float finalScale, float finalMultiplier,
final Runnable onStart, final Runnable onEnd) {
final float scale0 = mScale;
final float scale1 = finalScale;
final float bgMultiplier0 = mColorMultiplier;
final float bgMultiplier1 = finalMultiplier;
if (mScaleAnimator != null) {
mScaleAnimator.cancel();
}
mScaleAnimator = LauncherAnimUtils.ofFloat(0f, 1.0f);
mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
float prog = animation.getAnimatedFraction();
mScale = prog * scale1 + (1 - prog) * scale0;
mColorMultiplier = prog * bgMultiplier1 + (1 - prog) * bgMultiplier0;
invalidate();
}
});
mScaleAnimator.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationStart(Animator animation) {
if (onStart != null) {
onStart.run();
}
}
#Override
public void onAnimationEnd(Animator animation) {
if (onEnd != null) {
onEnd.run();
}
mScaleAnimator = null;
}
});
mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
mScaleAnimator.start();
}
public void animateToAccept(final CellLayout cl, final int cellX, final int cellY) {
Runnable onStart = new Runnable() {
#Override
public void run() {
delegateDrawing(cl, cellX, cellY);
}
};
animateScale(ACCEPT_SCALE_FACTOR, ACCEPT_COLOR_MULTIPLIER, onStart, null);
}
public void animateToRest() {
// This can be called multiple times -- we need to make sure the drawing delegate
// is saved and restored at the beginning of the animation, since cancelling the
// existing animation can clear the delgate.
final CellLayout cl = mDrawingDelegate;
final int cellX = delegateCellX;
final int cellY = delegateCellY;
Runnable onStart = new Runnable() {
#Override
public void run() {
delegateDrawing(cl, cellX, cellY);
}
};
Runnable onEnd = new Runnable() {
#Override
public void run() {
clearDrawingDelegate();
}
};
animateScale(1f, 1f, onStart, onEnd);
}
public int getBackgroundAlpha() {
return (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
}
public float getStrokeWidth() {
return mStrokeWidth;
}
}
Any help would be amazing
Thanks in advance :)
I am trying to achieve this circular progress bar with gradient intervals UI effect
Currently I have been able to achieve this.
The code for my current implementation is below.
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.graphics.drawable.AnimationDrawable;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public class CBPDrawable extends AnimationDrawable {
Paint paint = new Paint();
RectF clip = new RectF();
LinearGradient shader;
Path path;
private static final int MAX = 360;
int dots = 75;
int dotRadius = 20;
RectF dotRect;
Context mContext;
public CBPDrawable(Context context){
mContext = context;
}
Shader gradient;
private int mTickOffset = 0;
private int mTickLength = 15;
private int mArcRadius = 10;
double slope, startTickX, startTickY, endTickX, endTickY, midTickX, midTickY, thetaInRadians;
double radiusOffset = mArcRadius + mTickOffset;
#Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
path = new Path();
dotRect = new RectF(0, 0, dotRadius, dotRadius);
/*
for (int i = 0; i < dots; ++i) {
start();
path.addRoundRect(dotRect, dotRadius, dotRadius, Path.Direction.CCW);
stop();
}
*/
for (int i = 360; i >= 0; i -= 5) {
thetaInRadians = Math.toRadians(360 - i);
slope = Math.tan(thetaInRadians);
startTickX = (radiusOffset * Math.cos(thetaInRadians));
startTickY = slope * startTickX;
endTickX = startTickX + ((mTickLength) * Math.cos(thetaInRadians));
endTickY = slope * endTickX;
RectF r = new RectF();
r.set((float) startTickX, (float) startTickY, (float) endTickX, (float) endTickY);
path.addRoundRect(r, (int)radiusOffset, (int)radiusOffset, Path.Direction.CCW);
}
}
#Override
public void draw(#NonNull Canvas canvas) {
Rect b = getBounds();
final int width = canvas.getWidth();
final int height = canvas.getHeight();
final int squareSide = Math.min(width, height);
canvas.translate(width / 2f, height / 2f); // moving to the center of the View
canvas.rotate(270);
final float outerRadius = squareSide / 2f;
final float innerRadius = outerRadius - dotRadius;
final float angleFactor = 360f / 72;
int[] colors = new int[]{Color.BLUE, Color.RED};
float[] positions = new float[]{0,0.4f};
gradient = new SweepGradient(outerRadius / 2f, outerRadius / 2f,colors,null);
for (int i = 0; i < 72; ++i) {
canvas.save(); // creating a "checkpoint"
canvas.rotate(angleFactor * i);
canvas.translate(innerRadius, 0); //moving to the edge of the big circle
clip.set(b);
paint.setColor(Color.GRAY);
if(angleFactor * i <= 200){
paint.setShader(gradient);
}else{
paint.setShader(null);
}
//canvas.clipRect(clip, Region.Op.REPLACE);
canvas.drawPath(path, paint);
canvas.restore(); //restoring a "checkpoint"
//stop();
}
}
#Override
public void setAlpha(#IntRange(from = 0, to = 255) int i) {
}
#Override
public void setColorFilter(#Nullable ColorFilter colorFilter) {
}
#Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
}
As can be seen from the 2 images, I am unable to get the intervals correctly as a rectangle or line.
And, the gradient does not show, in spite of using a SweepGradient.
How should I go about achieving the desired effect?
Thanks.
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.
I used the android material design library to make a tab strip like this.
I added a view at the bottom of the screen like so.
Right now, I am trying to add the shadow effect from the top widget to the bottom view.
I found a solution in the form of an answer to this question.
The only way I found to create a top shadow was to modify some of the source of the Android compatibility v7 CardView project. This project brings the CardView class to older Android versions and thus also includes the elevation shadow. The resulting shadow is very close to a "real" elevation shadow.
I followed the instructions, and I ended with the following result to use an illustration
I added these colors to res/values/values.xml
<color name="cardview_shadow_end_color">#03000000</color>
<color name="cardview_shadow_start_color">#47000000</color>
<dimen name="cardview_compat_inset_shadow">1dp</dimen>
And I used the class RoundRectDrawableWithShadow from the modified Android compatibility v7 CardView project to set the shadow
float elevation = 200;
float density = 0.1f;
View bottomView = (View) rootView.findViewById(R.id.bottomView);
bottomView.setBackgroundDrawable(new RoundRectDrawableWithShadow(
getResources(), Color.BLACK, 0,
elevation * density, ((elevation + 1) * density) + 1
));
Here is my modifed CardView project code RoundRectDrawableWithShadow class
/*
* Copyright (C) 2014 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.
*/
import android.graphics.RectF;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
/**
* A rounded rectangle drawable which also includes a shadow around.
*/
public class RoundRectDrawableWithShadow extends Drawable {
// used to calculate content padding
final static double COS_45 = Math.cos(Math.toRadians(45));
final static float SHADOW_MULTIPLIER = 1.5f;
final int mInsetShadow; // extra shadow to avoid gaps between card and shadow
/*
* This helper is set by CardView implementations.
* <p>
* Prior to API 17, canvas.drawRoundRect is expensive; which is why we need this interface
* to draw efficient rounded rectangles before 17.
* */
static RoundRectHelper sRoundRectHelper;
Paint mPaint;
Paint mCornerShadowPaint;
Paint mEdgeShadowPaint;
final RectF mCardBounds;
float mCornerRadius;
Path mCornerShadowPath;
// updated value with inset
float mMaxShadowSize;
// actual value set by developer
float mRawMaxShadowSize;
// multiplied value to account for shadow offset
float mShadowSize;
// actual value set by developer
float mRawShadowSize;
private boolean mDirty = true;
private final int mShadowStartColor;
private final int mShadowEndColor;
private boolean mAddPaddingForCorners = true;
/**
* If shadow size is set to a value above max shadow, we print a warning
*/
private boolean mPrintedShadowClipWarning = false;
public RoundRectDrawableWithShadow(
Resources resources, int backgroundColor, float radius,
float shadowSize, float maxShadowSize
) {
mShadowStartColor = resources.getColor(R.color.cardview_shadow_start_color);
mShadowEndColor = resources.getColor(R.color.cardview_shadow_end_color);
mInsetShadow = resources.getDimensionPixelSize(R.dimen.cardview_compat_inset_shadow);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setColor(backgroundColor);
mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mCornerShadowPaint.setStyle(Paint.Style.FILL);
mCornerRadius = (int) (radius + .5f);
mCardBounds = new RectF();
mEdgeShadowPaint = new Paint(mCornerShadowPaint);
mEdgeShadowPaint.setAntiAlias(false);
setShadowSize(shadowSize, maxShadowSize);
RoundRectDrawableWithShadow.sRoundRectHelper
= new RoundRectDrawableWithShadow.RoundRectHelper() {
#Override
public void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius,
Paint paint) {
canvas.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
}
};
}
/**
* Casts the value to an even integer.
*/
private int toEven(float value) {
int i = (int) (value + .5f);
if (i % 2 == 1) {
return i - 1;
}
return i;
}
public void setAddPaddingForCorners(boolean addPaddingForCorners) {
mAddPaddingForCorners = addPaddingForCorners;
invalidateSelf();
}
#Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
mCornerShadowPaint.setAlpha(alpha);
mEdgeShadowPaint.setAlpha(alpha);
}
#Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mDirty = true;
}
void setShadowSize(float shadowSize, float maxShadowSize) {
if (shadowSize < 0 || maxShadowSize < 0) {
throw new IllegalArgumentException("invalid shadow size");
}
shadowSize = toEven(shadowSize);
maxShadowSize = toEven(maxShadowSize);
if (shadowSize > maxShadowSize) {
shadowSize = maxShadowSize;
if (!mPrintedShadowClipWarning) {
mPrintedShadowClipWarning = true;
}
}
if (mRawShadowSize == shadowSize && mRawMaxShadowSize == maxShadowSize) {
return;
}
mRawShadowSize = shadowSize;
mRawMaxShadowSize = maxShadowSize;
mShadowSize = (int)(shadowSize * SHADOW_MULTIPLIER + mInsetShadow + .5f);
mMaxShadowSize = maxShadowSize + mInsetShadow;
mDirty = true;
invalidateSelf();
}
#Override
public boolean getPadding(Rect padding) {
int vOffset = (int) Math.ceil(calculateVerticalPadding(mRawMaxShadowSize, mCornerRadius,
mAddPaddingForCorners));
// int hOffset = (int) Math.ceil(calculateHorizontalPadding(mRawMaxShadowSize, mCornerRadius,
// mAddPaddingForCorners));
// padding.set(hOffset, vOffset, hOffset, vOffset);
padding.set(0, vOffset, 0, 0);
return true;
}
static float calculateVerticalPadding(float maxShadowSize, float cornerRadius,
boolean addPaddingForCorners) {
if (addPaddingForCorners) {
return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius);
} else {
return maxShadowSize * SHADOW_MULTIPLIER;
}
}
static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius,
boolean addPaddingForCorners) {
if (addPaddingForCorners) {
return (float) (maxShadowSize + (1 - COS_45) * cornerRadius);
} else {
return maxShadowSize;
}
}
#Override
public void setColorFilter(ColorFilter cf) {
mPaint.setColorFilter(cf);
mCornerShadowPaint.setColorFilter(cf);
mEdgeShadowPaint.setColorFilter(cf);
}
#Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
void setCornerRadius(float radius) {
radius = (int) (radius + .5f);
if (mCornerRadius == radius) {
return;
}
mCornerRadius = radius;
mDirty = true;
invalidateSelf();
}
#Override
public void draw(Canvas canvas) {
if (mDirty) {
buildComponents(getBounds());
mDirty = false;
}
canvas.translate(0, -mRawShadowSize / 2);
drawShadow(canvas);
canvas.translate(0, +mRawShadowSize / 2);
sRoundRectHelper.drawRoundRect(canvas, mCardBounds, mCornerRadius, mPaint);
}
private void drawShadow(Canvas canvas) {
final float edgeShadowTop = -mCornerRadius - mShadowSize;
final float insetVertical = mCornerRadius + mInsetShadow + mRawShadowSize / 2;
final float insetHorizontal = -mInsetShadow;
// LT top
int saved = canvas.save();
canvas.translate(mCardBounds.left + insetHorizontal, mCardBounds.top + insetVertical);
canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
canvas.drawRect(0, edgeShadowTop,
mCardBounds.width() - 2 * insetHorizontal, -mCornerRadius + mShadowSize,
mEdgeShadowPaint);
canvas.restoreToCount(saved);
// RT right
saved = canvas.save();
canvas.translate(mCardBounds.right - insetHorizontal, mCardBounds.top + insetVertical);
canvas.rotate(90f);
canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
canvas.restoreToCount(saved);
}
private void buildShadowCorners() {
RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius);
RectF outerBounds = new RectF(innerBounds);
outerBounds.inset(-mShadowSize, -mShadowSize);
if (mCornerShadowPath == null) {
mCornerShadowPath = new Path();
} else {
mCornerShadowPath.reset();
}
mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
mCornerShadowPath.moveTo(-mCornerRadius, 0);
mCornerShadowPath.rLineTo(-mShadowSize, 0);
// outer arc
mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
// inner arc
mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
mCornerShadowPath.close();
float startRatio = mCornerRadius / (mCornerRadius + mShadowSize);
mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize,
new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
new float[]{0f, startRatio, 1f}
, Shader.TileMode.CLAMP));
// we offset the content shadowSize/2 pixels up to make it more realistic.
// this is why edge shadow shader has some extra space
// When drawing bottom edge shadow, we use that extra space.
mEdgeShadowPaint.setShader(new LinearGradient(0, -mCornerRadius + mShadowSize, 0,
-mCornerRadius - mShadowSize,
new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP));
mEdgeShadowPaint.setAntiAlias(false);
}
private void buildComponents(Rect bounds) {
// Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift.
// We could have different top-bottom offsets to avoid extra gap above but in that case
// center aligning Views inside the CardView would be problematic.
final float verticalOffset = mRawMaxShadowSize * SHADOW_MULTIPLIER;
mCardBounds.set(bounds.left + mRawMaxShadowSize, bounds.top + verticalOffset,
bounds.right - mRawMaxShadowSize, bounds.bottom - verticalOffset);
buildShadowCorners();
}
float getCornerRadius() {
return mCornerRadius;
}
void getMaxShadowAndCornerPadding(Rect into) {
getPadding(into);
}
void setShadowSize(float size) {
setShadowSize(size, mRawMaxShadowSize);
}
void setMaxShadowSize(float size) {
setShadowSize(mRawShadowSize, size);
}
float getShadowSize() {
return mRawShadowSize;
}
float getMaxShadowSize() {
return mRawMaxShadowSize;
}
float getMinWidth() {
final float content = 2 *
Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow + mRawMaxShadowSize / 2);
return content + (mRawMaxShadowSize + mInsetShadow) * 2;
}
float getMinHeight() {
final float content = 2 * Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow
+ mRawMaxShadowSize * SHADOW_MULTIPLIER / 2);
return content + (mRawMaxShadowSize * SHADOW_MULTIPLIER + mInsetShadow) * 2;
}
public void setColor(int color) {
mPaint.setColor(color);
invalidateSelf();
}
static interface RoundRectHelper {
void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius, Paint paint);
}
}
So the problem was that I was drawing a rectangle ove the entire view. The rectangle needs to be drawn on a seperate view dedicated to the shadow effect.
Additionally, the shadow view should have a height no greater than 6dp, or the rectangle will show, if that makes sense.
One last note, use these values for density and elevation
float elevation = 2;
float density = getResources().getDisplayMetrics().density;
Here is my layout
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="info.androidhive.materialtabs.fragments.OneFragment">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="This Is A Fragment"
android:gravity="center"
android:textSize="40dp"
android:textStyle="bold"
android:layout_centerInParent="true"
android:layout_above="#+id/bottomView"/>
<View
android:id="#+id/bottomView"
android:layout_width="match_parent"
android:layout_height="6dp"
android:layout_above="#+id/other"/>
<View
android:id="#id/other"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="175dp"
android:background="?attr/colorPrimary"/>
</RelativeLayout>
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.