I am able to draw custom back ground to xml file using Shape
But how to add arc or curv at specified place.
In my library Facebook Like Button I implemented custom Path in order to achieve this goal.
There is ability to specify location of marker wherever you want:
Code from source:
import android.graphics.Path;
import android.graphics.RectF;
public class CalloutPath extends Path {
public static final int MARKER_NONE = 0x0;
public static final int MARKER_LEFT = 0x1;
public static final int MARKER_TOP = 0x2;
public static final int MARKER_RIGHT = 0x4;
public static final int MARKER_BOTTOM = 0x8;
public static final int MARKER_ALL = 0xf;
private final RectF oval = new RectF();
/**
* #param m marker
* #param w width
* #param h height
* #param s stroke thickness
* #param r corners radius
*/
public void build(int m, float w, float h, float s, float r) {
int fl = factor(m, MARKER_LEFT);
int ft = factor(m, MARKER_TOP);
int fr = factor(m, MARKER_RIGHT);
int fb = factor(m, MARKER_BOTTOM);
float x0 = s + 0f;
float x1 = s + r * fl;
float x2 = s + r + r * fl;
float x3 = w / 2f - r;
float x4 = w / 2f;
float x5 = w / 2f + r;
float x6 = w - 1f - s - r - r * fr;
float x7 = w - 1f - s - r * fr;
float x8 = w - 1f - s;
float y0 = s + 0f;
float y1 = s + r * ft;
float y2 = s + r + r * ft;
float y3 = h / 2f - r;
float y4 = h / 2f;
float y5 = h / 2f + r;
float y6 = h - 1f - s - r - r * fb;
float y7 = h - 1f - s - r * fb;
float y8 = h - 1f - s;
reset();
moveTo(x1, y2);
oval.set(x2 - r, y2 - r, x2 + r, y2 + r);
arcTo(oval, 180f, 90f);
if (ft != 0) {
lineTo(x3, y1);
lineTo(x4, y0);
lineTo(x5, y1);
}
lineTo(x6, y1);
oval.set(x6 - r, y2 - r, x6 + r, y2 + r);
arcTo(oval, 270f, 90f);
if (fr != 0) {
lineTo(x7, y3);
lineTo(x8, y4);
lineTo(x7, y5);
}
lineTo(x7, y6);
oval.set(x6 - r, y6 - r, x6 + r, y6 + r);
arcTo(oval, 0f, 90f);
if (fb != 0) {
lineTo(x5, y7);
lineTo(x4, y8);
lineTo(x3, y7);
}
lineTo(x2, y7);
oval.set(x2 - r, y6 - r, x2 + r, y6 + r);
arcTo(oval, 90f, 90f);
if (fl != 0) {
lineTo(x1, y5);
lineTo(x0, y4);
lineTo(x1, y3);
}
close();
}
public static int factor(int marker, int mask) {
return (marker & mask) != 0 ? 1 : 0;
}
}
Because you have this arc or curv you should create a custom jpg.
But you can use with corners and have below :
You do not need to do it programmatically, you should just use a nine patch image resource that make a perfect image for you. If you want to know more about nine patch and how it works, have a look at this and this links.
As you can see this image makes a perfect chat bubble for you. I already decompiled Whatsapp , Viber and they all using a nine patched image to make a chat bubble. As you can see, the Telegram app also using a nine patched image to achieve this.
My sample chat bubble image:
I also should mention that you don't need any custom path. It is a typical patched image without any customization.
make 9patch image like this so that if in conversation lots of text then also it can be adjusted itself and also not stretch or compressed from curve .
Hope it will help you
Related
I am trying to create a spray can tool in Android for a Graffiti app. I want to achieve something similar done in the below image
Dripping Spray Can Sample image
Requirements: If somebody is spraying the can then it should drip some color downwards while drawing a spray line
I am currently using the following code, but not getting the correct results:
if (bufferPaint == null) {
bufferPaint = new Paint();
bufferPaint.setAntiAlias(true);
bufferPaint.setDither(true);
bufferPaint.setXfermode(null);
if (mPaint == null)
setupColor(0f, 100f);
}
int alpha = Math.round((float) 100 / 100 * 255);
bufferPaint.setAlpha(alpha);
private void paintOneDab(float x, float y) {
float radius = 5 / 2f;
float scatter = 500 / 100f;
x += radius 2 scatter (1 - 2 (float) Math.random());
y += radius 2 scatter (1 - 2 (float) Math.random());
float hardness = 20 / 100f;
positions[0] = 0;
positions[1] = (float) (Math.sqrt(hardness));
positions[2] = 1;
bufferPaint.setShader(new RadialGradient(x, y, radius, colors, positions, Shader.TileMode.CLAMP));
mCanvas.save();
int angle = 180;
mCanvas.rotate(angle, x, y);
int roundness = 100;
mCanvas.scale(1f, 100f / roundness, x, y);
mCanvas.drawCircle(x, y, radius, bufferPaint);
mCanvas.restore();
}
I am trying to draw spiral view as in screenshot attached,
I have array of points to draw based on number on circles.
I am new to android and kotlin .
Can anyone please suggest how to do this?
Today I added spiral drawing to this custom scrolling shapes app I'm building. My implementation makes use of Android import android.graphics.Path;. The spiral drawing is based on the user scrolling, with more segments added as the user scrolls.
The gist of the solution is as follows: I start with an initial arc and draw that. That arc is the initial arc of the spiral. On that arc has been added to the path, then use the bounding Rect of the Path to determine the bounds for the next segment of the arc.
The below is a sample but for full context you'll need to view the project: https://github.com/jdgreene2008/android_custom_views
private void addTopSpiralSegment(Path path, RectF bounds, SpiralSegment spiralSegment) {
float centerX = bounds.centerX();
float centerY = bounds.centerY();
float segmentBoundsLeft;
float segmentBoundsRight;
float segmentBoundsTop;
float segmentBoundsBottom;
if (path.isEmpty()) {
segmentBoundsLeft = centerX - spiralSegment.getWidth() / 2;
segmentBoundsRight = centerX + spiralSegment.getWidth() / 2;
segmentBoundsTop = centerY - spiralSegment.getHeight() / 2;
segmentBoundsBottom = centerY + spiralSegment.getHeight() / 2;
} else {
RectF pathBounds = new RectF();
path.computeBounds(pathBounds, true);
segmentBoundsLeft = pathBounds.left;
segmentBoundsRight = segmentBoundsLeft + spiralSegment.getWidth();
segmentBoundsTop = centerY - spiralSegment.getHeight() / 2;
segmentBoundsBottom = centerY + spiralSegment.getHeight() / 2;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
path.addArc(segmentBoundsLeft, segmentBoundsTop, segmentBoundsRight,
segmentBoundsBottom, 180, 180);
}
}
private void addBottomSpiralSegment(Path path, RectF bounds, SpiralSegment spiralSegment) {
float centerX = bounds.centerX();
float centerY = bounds.centerY();
float segmentBoundsLeft;
float segmentBoundsRight;
float segmentBoundsTop;
float segmentBoundsBottom;
if (path.isEmpty()) {
segmentBoundsLeft = centerX - spiralSegment.getWidth() / 2;
segmentBoundsRight = centerX + spiralSegment.getWidth() / 2;
segmentBoundsTop = centerY - spiralSegment.getHeight() / 2;
segmentBoundsBottom = centerY + spiralSegment.getHeight() / 2;
} else {
RectF pathBounds = new RectF();
path.computeBounds(pathBounds, true);
segmentBoundsLeft = pathBounds.right - spiralSegment.getWidth();
segmentBoundsRight = pathBounds.right;
segmentBoundsTop = centerY - spiralSegment.getHeight() / 2;
segmentBoundsBottom = centerY + spiralSegment.getHeight() / 2;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
path.addArc(segmentBoundsLeft, segmentBoundsTop, segmentBoundsRight,
segmentBoundsBottom, 0, 180);
}
}
A while back I found this great color picker from Piotr Adams which I can not find on Git anymore but it's still on this page: https://www.programcreek.com/java-api-examples/index.php?source_dir=Random-Penis-master/app/src/main/java/com/osacky/penis/picker/ColorPicker.java
The main reason I use this color picker in my app is because I want to be able to place a pointer on the RadialGradient based on a color. This library calculates the position for a certain color, this means placing a picker on the correct location is very fast and easy.
The problem is I don't quite understand how it works. I now want to generate a RadialGradient with different colors. But the logic it uses does not work when I generate a RadialGradient with different colors.
Here is the code that generates the RadialGradient:
private Bitmap createColorWheelBitmap(int width, int height) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
int colorCount = 12;
int colorAngleStep = 360 / 12;
int colors[] = new int[colorCount + 1];
float hsv[] = new float[]{0f, 1f, 1f};
for (int i = 0; i < colors.length; i++) {
hsv[0] = (i * colorAngleStep + 180) % 360;
colors[i] = Color.HSVToColor(hsv);
}
colors[colorCount] = colors[0];
SweepGradient sweepGradient = new SweepGradient(width / 2, height / 2, colors, null);
RadialGradient radialGradient = new RadialGradient(width / 2, height / 2, colorWheelRadius, 0xFFFFFFFF, 0x00FFFFFF, TileMode.CLAMP);
ComposeShader composeShader = new ComposeShader(sweepGradient, radialGradient, PorterDuff.Mode.SRC_OVER);
colorWheelPaint.setShader(composeShader);
Canvas canvas = new Canvas(bitmap);
canvas.drawCircle(width / 2, height / 2, colorWheelRadius, colorWheelPaint);
return bitmap;
}
The code for listening to changes of the picker, so this calculates the color based on a position:
#Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
int x = (int) event.getX();
int y = (int) event.getY();
int cx = x - getWidth() / 2;
int cy = y - getHeight() / 2;
double d = Math.sqrt(cx * cx + cy * cy);
if (d <= colorWheelRadius) {
colorHSV[0] = (float) (Math.toDegrees(Math.atan2(cy, cx)) + 180f);
colorHSV[1] = Math.max(0f, Math.min(1f, (float) (d / colorWheelRadius)));
selectedPointer.setColor(Color.HSVToColor(colorHSV));
notifyListeners();
invalidate();
}
return true;
case MotionEvent.ACTION_BUTTON_PRESS:
}
return super.onTouchEvent(event);
}
Finally the code that calculates the position based on a color:
// drawing color wheel pointer
float hueAngle = (float) Math.toRadians(colorHSV[0]);
int colorPointX = (int) (-Math.cos(hueAngle) * colorHSV[1] * colorWheelRadius) + centerX;
int colorPointY = (int) (-Math.sin(hueAngle) * colorHSV[1] * colorWheelRadius) + centerY;
float pointerRadius = 0.075f * colorWheelRadius;
int pointerX = (int) (colorPointX - pointerRadius / 2);
int pointerY = (int) (colorPointY - pointerRadius / 2);
colorPointerCoords.set(pointerX, pointerY, pointerX + pointerRadius, pointerY + pointerRadius);
canvas.drawOval(colorPointerCoords, colorPointerPaint);
So my question is how can I for example change the RadialGradient to only include 2 colors, without breaking the calculations of getting the color? Even an explanation on how this works would be great!
There is great tutorial here: http://tekeye.uk/android/examples/ui/android-color-picker-tutorial (not mine). I don't know much about the theory behind it either but you can use this code to calculate color based on position.
// Calculate channel based on 2 surrounding colors and p angle.
private int ave(int s, int d, float p) {
return s + java.lang.Math.round(p * (d - s));
}
// Calculate color based on drawn colors and angle based on x and y position.
private int interpColor(int colors[], float unit) {
if (unit <= 0) {
return colors[0];
}
if (unit >= 1) {
return colors[colors.length - 1];
}
// Adjust the angle (unit) based on how many colors there are in the list.
float p = unit * (colors.length - 1);
// Get starting color position in the array.
int i = (int)p;
p -= i;
// Now p is just the fractional part [0...1) and i is the index.
// Get two composite colors for calculations.
int c0 = colors[i];
int c1 = colors[i+1];
// Calculate color channels.
int a = ave(Color.alpha(c0), Color.alpha(c1), p);
int r = ave(Color.red(c0), Color.red(c1), p);
int g = ave(Color.green(c0), Color.green(c1), p);
int b = ave(Color.blue(c0), Color.blue(c1), p);
// And finally create the color from the channels.
return Color.argb(a, r, g, b);
}
You can call the interpreting function like this for example.
#Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX() - CENTER_X;
float y = event.getY() - CENTER_Y;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
// Calculate the angle based on x and y positions clicked.
float angle = (float)java.lang.Math.atan2(y, x);
// need to turn angle [-PI ... PI] into unit [0....1]
float unit = angle/(2*PI);
if (unit < 0) {
unit += 1;
}
// mColors is your list with colors so int[].
int color = interpColor(mColors, unit);
break;
}
}
I already tried it in my project and it works like a charm. So hope it helps you too. :)
EDIT:
Oh so my colors are set up like this.
mColors = intArrayOf(-0x10000, -0xff01, -0xffff01, -0xff0001, -0xff0100, -0x100, -0x10000)
So you can add/remove as many colors as you want and since the interpret functions calculates based on size of this array it should work.
How can I draw Path with fading (opacity or thicknes) line? Something like this.
I know there is LinearGradient shader for Paint, but it won't bend along the Path.
One possible solution might be to get points along the Path and just draw it by myself through the segments`. But I coouldn't find any method for that either.
I came up with the following code. The mos important thing is PathMeasure's getPosTan() method.
if (getGesturePath() != null) {
final short steps = 150;
final byte stepDistance = 5;
final byte maxTrailRadius = 15;
pathMeasure.setPath(getGesturePath(), false);
final float pathLength = pathMeasure.getLength();
for (short i = 1; i <= steps; i++) {
final float distance = pathLength - i * stepDistance;
if (distance >= 0) {
final float trailRadius = maxTrailRadius * (1 - (float) i / steps);
pathMeasure.getPosTan(distance, pathPos, null);
final float x = pathPos[0] + RandomUtils.nextFloat(0, 2 * trailRadius) - trailRadius;
final float y = pathPos[1] + RandomUtils.nextFloat(0, 2 * trailRadius) - trailRadius;
paint.setShader(new RadialGradient(
x,
y,
trailRadius > 0 ? trailRadius : Float.MIN_VALUE,
ColorUtils.setAlphaComponent(Color.GREEN, random.nextInt(0xff)),
Color.TRANSPARENT,
Shader.TileMode.CLAMP
));
canvas.drawCircle(x, y, trailRadius, paint);
}
}
}
I'm using Android's android.graphics.Canvas class to draw a ring. My onDraw method clips the canvas to make a hole for the inner circle, and then draws the full outer circle over the hole:
clip = new Path();
clip.addRect(outerCircle, Path.Direction.CW);
clip.addOval(innerCircle, Path.Direction.CCW);
canvas.save();
canvas.clipPath(clip);
canvas.drawOval(outerCircle, lightGrey);
canvas.restore();
The result is a ring with a pretty, anti-aliased outer edge and a jagged, ugly inner edge:
What can I do to antialias the inner edge?
I don't want to cheat by drawing a grey circle in the middle because the dialog is slightly transparent. (This transparency isn't as subtle on on other backgrounds.)
As far as I know, you can't antialias clip regions.
I'd suggest using bitmap masking instead. Render the the pink, white, and light gray foreground to one bitmap, render the outer/inner circle mask (the grayscale alpha channel) to another bitmap, and then use Paint.setXfermode to render the foreground bitmap with the mask as its alpha channel.
An example can be found in the ApiDemos source code here.
I know this is not a general answer, but in this particular case, you could draw arcs with a thick stroke width, instead of the circles + mask.
I too had this same problem. I tried using Bitmap masking (xFermode) to fix the Aliasing, but it was heavy.
So for API < 19, I used the Bitmap masking way and for API >= 19, I used Path.Op. Instead of clipping the path and then drawing the shape. I did a REVERSE_DIFFERENCE of the path and the shape(which is of type Path). You can perform operations on Path from API 19 and above.
Works perfectly for me!
you can try the following code:
public class GrowthView extends View {
private static final String TAG = "GrowthView";
private int bgColor = Color.parseColor("#33485d");
private int valColor = Color.parseColor("#ecb732");
private int[] scores = new int[]{0, 10, 80, 180, 800, 5000, 20000, 50000, 100000};
private Context mContext;
private float w;
private float h;
private Paint bgPaint;
private Paint growthPaint;
private Paint textPaint;
private Paint clipPaint;
private Path bgPath;
private Path bgClipPath;
private Path growthPath;
private int growthValue = 0;
private float bgFullAngle = 240.0f;
private float gapAngle = bgFullAngle / (scores.length - 1);
private float gapRadius = 21.5f;//实际为21px 略大半个像素避免path无法缝合error
private float outerRadius = 240.0f;
private float innerRadius = outerRadius - gapRadius * 2;
private RectF outerRecF;
private RectF innerRecF;
private RectF leftBoundRecF;
private RectF rightBoundRecF;
public GrowthView(Context context) {
this(context, null);
}
public GrowthView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public GrowthView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.mContext = context;
init();
}
private void init() {
Xfermode xFermode = new PorterDuffXfermode(PorterDuff.Mode.DARKEN);
bgPaint = new Paint();
bgPaint.setStyle(Paint.Style.FILL);
bgPaint.setColor(bgColor);
bgPaint.setStrokeWidth(0.1f);
bgPaint.setAntiAlias(true);
growthPaint = new Paint();
growthPaint.setStyle(Paint.Style.FILL_AND_STROKE);
growthPaint.setColor(valColor);
growthPaint.setStrokeWidth(1f);
growthPaint.setAntiAlias(true);
clipPaint = new Paint();
clipPaint.setStyle(Paint.Style.FILL);
clipPaint.setColor(Color.WHITE);
clipPaint.setStrokeWidth(.1f);
clipPaint.setAntiAlias(true);
clipPaint.setXfermode(xFermode);
textPaint = new Paint();
textPaint.setTextSize(96);//todo comfirm the textSize
textPaint.setStrokeWidth(1f);
textPaint.setAntiAlias(true);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setColor(valColor);
bgPath = new Path();
growthPath = new Path();
//todo 暂定中心点为屏幕中心
DisplayMetrics metrics = new DisplayMetrics();
WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(metrics);
w = metrics.widthPixels;
h = metrics.heightPixels;
outerRecF = new RectF(w / 2 - outerRadius, h / 2 - outerRadius, w / 2 + outerRadius, h / 2 + outerRadius);
innerRecF = new RectF(w / 2 - innerRadius, h / 2 - innerRadius, w / 2 + innerRadius, h / 2 + innerRadius);
rightBoundRecF = new RectF(w / 2 + (float) Math.pow(3, 0.5) * (innerRadius + gapRadius) / 2 - gapRadius,
h / 2 + (innerRadius + gapRadius) / 2 - gapRadius,
w / 2 + (float) Math.pow(3, 0.5) * (innerRadius + gapRadius) / 2 + gapRadius,
h / 2 + (innerRadius + gapRadius) / 2 + gapRadius);
leftBoundRecF = new RectF(w / 2 - (float) Math.pow(3, 0.5) * (innerRadius + gapRadius) / 2 - gapRadius,
h / 2 + (innerRadius + gapRadius) / 2 - gapRadius,
w / 2 - (float) Math.pow(3, 0.5) * (innerRadius + gapRadius) / 2 + gapRadius,
h / 2 + (innerRadius + gapRadius) / 2 + gapRadius);
bgClipPath = new Path();
bgClipPath.arcTo(innerRecF, 150.0f, 359.9f, true);
bgClipPath.close();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//bg
float startAngle = 150.0f;
float endRecfFullAngle = 180.0f;
bgPath.arcTo(outerRecF, startAngle, bgFullAngle, true);
bgPath.arcTo(rightBoundRecF, 30.0f, endRecfFullAngle, true);
bgPath.arcTo(innerRecF, startAngle, bgFullAngle);
bgPath.arcTo(leftBoundRecF, -30.0f, endRecfFullAngle);
bgPath.rMoveTo(w / 2 - outerRadius * (float) Math.pow(3, 0.5) / 2, h / 2 + outerRadius / 2);
bgPath.setFillType(Path.FillType.WINDING);
bgPath.close();
//growth
if (getGrowthVal() != 0) {
float temp = getGrowthAngle(getGrowthVal());
growthPath.arcTo(outerRecF, startAngle, temp, true);
growthPath.arcTo(getDynamicRecF(getGrowthVal()), getDynamicOriginAngle(getGrowthVal()), endRecfFullAngle, true);
growthPath.arcTo(innerRecF, startAngle, temp);
growthPath.arcTo(leftBoundRecF, -30.0f, endRecfFullAngle);
growthPath.rMoveTo(w / 2 - outerRadius * (float) Math.pow(3, 0.5) / 2, h / 2 + outerRadius / 2);
growthPath.close();
}
canvas.drawText(formatVal(getGrowthVal()), w / 2, h / 2, textPaint);
canvas.clipPath(bgClipPath, Region.Op.DIFFERENCE);
canvas.drawPath(bgPath, bgPaint);
canvas.drawPath(growthPath, growthPaint);
canvas.drawPath(bgClipPath, clipPaint);
}
private float getDynamicOriginAngle(int growthVal) {
return growthVal <= 30 ? getGrowthAngle(growthVal) + 150 :
getGrowthAngle(growthVal) - 210;
}
private RectF getDynamicRecF(int growthVal) {
float dynamicAngle = getGrowthAngle(growthVal);
//动态圆心
float _w = w / 2 + (float) Math.sin(Math.toRadians(dynamicAngle - 120)) * (outerRadius - gapRadius);
float _y = h / 2 - (float) Math.sin(Math.toRadians(dynamicAngle - 30)) * (outerRadius - gapRadius);
return new RectF(_w - gapRadius, _y - gapRadius, _w + gapRadius, _y + gapRadius);
}
private int getGrowthVal() {
return this.growthValue;
}
public void setGrowthValue(int value) {
if (value < 0 || value > 100000) {
try {
throw new Exception("成长值不在范围内");
} catch (Exception e) {
Log.e(TAG, e.getMessage());
e.printStackTrace();
}
}
this.growthValue = value;
invalidate();
}
private float getGrowthAngle(int growthVal) {
return gapAngle * (getLevel(growthVal) - 1)
+ gapAngle * (growthVal - scores[getLevel(growthVal) - 1]) /
(scores[getLevel(growthVal)] - scores[getLevel(growthVal) - 1]);
}
private int getLevel(int score) {
return score < 0 ? -1 : score <= 10 ? 1 : score <= 80 ? 2 : score <= 180 ? 3 : score <= 800 ?
4 : score <= 5000 ? 5 : score <= 20000 ? 6 : score <= 50000 ? 7 : 8;
}
private String formatVal(int value) {
StringBuilder builder = new StringBuilder(String.valueOf(value));
return value < 1000 ? builder.toString() : builder.insert(builder.length() - 3, ',').toString();
}
}
Use the Xfermode Api with canvas.clipPath() may resolve this problem...
Result