问题
I've created triangle shape as in code below:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<rotate
android:fromDegrees="45"
android:toDegrees="45"
android:pivotX="13%"
android:pivotY="-40%" >
<shape
android:shape="rectangle" >
<stroke android:color="#000000" android:width="1dp"/>
<solid
android:color="#000000" />
</shape>
</rotate>
</item>
</layer-list>
How can I make border color of the triangle different from the rest of shape? If I change stroke color it kinda works, well I have two sides with different color, without third border. How should I correct it?
回答1:
Now it did take me some time to try to make a good example for you to play around with canvas. Following code can still be improved with linear/radial gradients, also you can make it more customizable if you wish. Maybe I will do it in the future, but I'm done for today.
First add this to your resource values. I use /values/attrs.xml If you do not see this file, create it.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="Triangle">
<attr name="triangleColor" format="color"/>
<attr name="triangleStrokeColor" format="color" />
<attr name="triangleStrokeWidth" format="dimension" />
</declare-styleable>
</resources>
Now create a Triangle class, preferably in a folder where you keep your custom views. Fix package name and the R class import.
package si.kseneman.views;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.View;
import si.kseneman.mobile.R;
public class Triangle extends View {
private int measuredWidth, measuredHeight;
private float density;
private Paint mTrianglePaint, mStrokePaint;
private PointF a, b, c;
private Path mTrianglePath;
private float mStrokeWidth;
// Default values
private int mTriangleColor = 0xAA4CAF50; //ARGB int
private int mStrokeColor = Color.BLACK; //ARGB int
private float defaultPadding = 5; //dp
public Triangle(Context context) {
super(context);
init(context, null, 0);
}
public Triangle(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
public Triangle(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(Context context, AttributeSet attrs, int style) {
Resources res = context.getResources();
density = res.getDisplayMetrics().density;
defaultPadding *= density;
// Get the values from XML
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.Triangle, style, 0);
int tmp;
mTriangleColor = typedArray.getColor(R.styleable.Triangle_triangleColor, mTriangleColor);
mStrokeColor = typedArray.getColor(R.styleable.Triangle_triangleStrokeColor, mStrokeColor);
tmp = typedArray.getDimensionPixelSize(R.styleable.Triangle_triangleStrokeWidth, -1);
mStrokeWidth = tmp != -1 ? tmp : 2 * density; // Use 2dp as a default value
typedArray.recycle();
a = new PointF();
b = new PointF();
c = new PointF();
mTrianglePath = new Path();
mTrianglePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTrianglePaint.setStyle(Paint.Style.FILL);
mTrianglePaint.setColor(mTriangleColor);
mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mStrokePaint.setStyle(Paint.Style.STROKE);
mStrokePaint.setStrokeWidth(mStrokeWidth);
mStrokePaint.setColor(mStrokeColor);
}
public void setTriangleColorResId(int resId) {
// Example: setTriangleColorResId(R.color.blue);
setTriangleColor(getContext().getResources().getColor(resId));
}
public void setTriangleColor(String color) {
setTriangleColor(Color.parseColor(color));
}
public void setTriangleColor(int color) {
mTriangleColor = color;
mTrianglePaint.setColor(mTriangleColor);
invalidate();
}
public void setStrokeColorResId(int resId) {
// Example: setTriangleColorResId(R.color.blue);
setStrokeColor(getContext().getResources().getColor(resId));
}
public void setStrokeColor(String color) {
setTriangleColor(Color.parseColor(color));
}
public void setStrokeColor(int color) {
mStrokeColor = color;
mStrokePaint.setColor(mStrokeColor);
invalidate();
}
public void setStrokeWidth(float strokeWidth) {
if (strokeWidth < 0)
throw new IllegalArgumentException("Stroke width cannot be < 0");
//NOTE: input parameter is meant to be in pixels, you need to convert dp, sp or anything else
// when calling this method
mStrokeWidth = strokeWidth;
mStrokePaint.setStrokeWidth(mStrokeWidth);
invalidate();
}
public int getStrokeColor() {
return mStrokeColor;
}
public float getStrokeWidth() {
return mStrokeWidth;
}
public int getTriangleColor() {
return mTriangleColor;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measuredHeight = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
measuredWidth = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
setMeasuredDimension(measuredWidth, measuredHeight);
//Log.d(TAG, "Height: " + measuredHeight + " Width: " + measuredWidth);
}
private float getPaddingOrDefault(int padding, float defaultValue) {
return padding != 0 ? padding : defaultValue;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (measuredHeight <= 0 || measuredWidth <= 0) {
// Not much we can draw... :/
return;
}
// Define the points of the triangle... make this so that it suits your needs
// Top point
a.x = measuredWidth / 2f;
a.y = getPaddingOrDefault(getPaddingTop(), defaultPadding);
// Bottom left point
b.x = getPaddingOrDefault(getPaddingLeft(), defaultPadding);
b.y = measuredHeight - getPaddingOrDefault(getPaddingBottom(), defaultPadding);
// Bottom right point
c.x = measuredWidth - getPaddingOrDefault(getPaddingRight(), defaultPadding);
c.y = b.y;
// Clear the path from previous onDraw
mTrianglePath.reset();
// Make a path of triangle
mTrianglePath.moveTo(a.x, a.y);
mTrianglePath.lineTo(b.x, b.y);
mTrianglePath.lineTo(c.x, c.y);
mTrianglePath.lineTo(a.x, a.y);
mTrianglePath.close();
// Draw the triangle and the stroke
canvas.drawPath(mTrianglePath, mTrianglePaint);
canvas.drawPath(mTrianglePath, mStrokePaint);
}
}
And now crate a new class (this is not neeeded, but I did so that you might get the idea how to use canvas)
package si.kseneman.views;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.View;
public class TriangleAndArc extends View {
private int measuredWidth, measuredHeight;
private float density;
private float a1Degrees;
private float textHeight;
private double a1Radians;
private Paint mLinePaint, mPointPaint, mTextPaint, mCirclePaint;
private DashPathEffect dashedEffect;
private PointF p1, p2, m, c;
public TriangleAndArc(Context context) {
super(context);
init(context);
}
public TriangleAndArc(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public TriangleAndArc(Context context, AttributeSet attrs, int style) {
super(context, attrs, style);
init(context);
}
private void init(Context ctx) {
p1 = new PointF();
p2 = new PointF();
c = new PointF();
m = new PointF();
Resources res = ctx.getResources();
density = res.getDisplayMetrics().density;
a1Degrees = 36.0f;
a1Radians = Math.toRadians(a1Degrees);
dashedEffect = new DashPathEffect(new float[]{5, 5}, 1.0f);
mLinePaint = new Paint();
mLinePaint.setAntiAlias(true);
mLinePaint.setStyle(Paint.Style.STROKE);
mLinePaint.setStrokeJoin(Paint.Join.ROUND);
mLinePaint.setStrokeWidth(2 * density);
mLinePaint.setColor(Color.BLACK);
//mPaint.setPathEffect(dashedEffect);
mPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPointPaint.setStyle(Paint.Style.FILL);
mPointPaint.setColor(Color.RED);
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setTextSize(10 * density);
mTextPaint.setTypeface(Typeface.create("Roboto-Thin", Typeface.BOLD));
mTextPaint.setColor(Color.RED);
if(!isInEditMode()) {
// Shadow layer is not supported in preview mode and android studio makes an ugly warning about it!
mTextPaint.setShadowLayer(0.1f, 0, 1, Color.GRAY);
}
textHeight = Math.abs(mTextPaint.descent() + mTextPaint.ascent());
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint.setColor(Color.BLUE);
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setStrokeWidth(density);
}
// Square a number
private double sq(double a) {
return a * a;
}
private void drawIncenterAndExcenter(PointF A, PointF B, PointF C, Canvas canvas) {
double a = Math.sqrt((B.x - C.x) * (B.x - C.x) + (B.y - C.y) * (B.y - C.y));
double b = Math.sqrt((A.x - C.x) * (A.x - C.x) + (A.y - C.y) * (A.y - C.y));
double c = Math.sqrt((A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y));
double perimeter = a + b + c;
double s = 0.5 * perimeter;
double area = Math.sqrt(s * (s - a) * (s - b) * (s - c));
// Inscribed Circle
double iRadius = area / s;
PointF iCenter = new PointF();
iCenter.x = (float) ((a * A.x + b * B.x + c * C.x) / (perimeter));
iCenter.y = (float) ((a * A.y + b * B.y + c * C.y) / (perimeter));
// Circumscribed Circle
PointF cCenter = new PointF();
double cRadius = (a * b * c) / (4.0 * area);
double D = 2 * (A.x * (B.y - C.y) + B.x * (C.y - A.y) + C.x * (A.y - B.y));
double sqA = sq(A.x) + sq(A.y);
double sqB = sq(B.x) + sq(B.y);
double sqC = sq(C.x) + sq(C.y);
cCenter.x = (float) ((sqA * (B.y - C.y) + sqB * (C.y - A.y) + sqC * (A.y - B.y)) / D);
cCenter.y = (float) ((sqA * (C.x - B.x) + sqB * (A.x - C.x) + sqC * (B.x - A.x)) / D);
// Draw
canvas.drawCircle(iCenter.x, iCenter.y, density * 5, mPointPaint);
canvas.drawCircle(iCenter.x, iCenter.y, (float) iRadius, mCirclePaint);
canvas.drawText("I", iCenter.x - mTextPaint.measureText("I") * 0.5f, iCenter.y - textHeight - 2 * density, mTextPaint);
canvas.drawCircle(cCenter.x, cCenter.y, density * 5, mPointPaint);
canvas.drawCircle(cCenter.x, cCenter.y, (float) cRadius, mCirclePaint);
canvas.drawText("E", cCenter.x - mTextPaint.measureText("E") * 0.5f, cCenter.y - textHeight - 2 * density, mTextPaint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measuredHeight = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
measuredWidth = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
setMeasuredDimension(measuredWidth, measuredHeight);
//Log.d(TAG, "Height: " + measuredHeight + " Width: " + measuredWidth);
}
@Override
protected void onDraw(Canvas canvas) {
if (measuredHeight <= 0 || measuredWidth <= 0) {
// Not much we can draw... :/
return;
}
// Orientation independent drawing
int width = (measuredWidth > measuredHeight) ? measuredWidth : measuredHeight;
int height = (width == measuredWidth) ? measuredHeight : measuredWidth;
// Define points
p1.x = width * 0.7f;
p1.y = height * 0.2f - 20;
p2.x = width * 0.85f;
p2.y = 2 * height / 3.0f - 20;
float dx = p2.x - p1.x;
float dy = p2.y - p1.y;
// l1 is half the length of the line from p1 to p2
double l = Math.sqrt(dx * dx + dy * dy);
double l1 = l / 2.0;
// Center of the circle
double h = l1 / (Math.tan(a1Radians / 2.0));
// Radius of the circle
double r = l1 / (Math.sin(a1Radians / 2.0));
// a2 is the angle at which L intersects the x axis
double a2 = Math.atan2(dy, dx);
// a3 is the angle at which H intersects the x axis
double a3 = (Math.PI / 2.0) - a2;
// m is the midpoint of the line from e1 to e2
m.x = (p1.x + p2.x) / 2.0f;
m.y = (p1.y + p2.y) / 2.0f;
// c is the the center of the circle
c.x = (float) (m.x - (h * Math.cos(a3)));
c.y = (float) (m.y + (h * Math.sin(a3)));
// rect is the square RectF that bounds the "oval"
RectF oval = new RectF((float) (c.x - r), (float) (c.y - r), (float) (c.x + r), (float) (c.y + r));
// a4 is the starting sweep angle
double rawA4 = Math.atan2(p1.y - c.y, p1.x - c.x);
float a4 = (float) Math.toDegrees(rawA4);
// Draw lines
canvas.drawLine(p1.x, p1.y, p2.x, p2.y, mLinePaint);
canvas.drawLine(c.x, c.y, p1.x, p1.y, mLinePaint);
canvas.drawLine(c.x, c.y, p2.x, p2.y, mLinePaint);
canvas.drawLine(c.x, c.y, m.x, m.y, mLinePaint);
// Draw arc
mLinePaint.setPathEffect(dashedEffect);
canvas.drawArc(oval, a4, a1Degrees, false, mLinePaint);
// Draw dots
canvas.drawCircle(p1.x, p1.y, density * 5, mPointPaint);
canvas.drawCircle(p2.x, p2.y, density * 5, mPointPaint);
canvas.drawCircle(m.x, m.y, density * 5, mPointPaint);
canvas.drawCircle(c.x, c.y, density * 5, mPointPaint);
// Draw text
float halfOfTextHeight = textHeight * 0.5f; // We need an offset of a half
canvas.drawText("p1", p1.x + density * 7, p1.y + halfOfTextHeight, mTextPaint);
canvas.drawText("p2", p2.x + density * 7, p2.y + halfOfTextHeight, mTextPaint);
canvas.drawText("m", m.x + density * 7, m.y + halfOfTextHeight, mTextPaint);
canvas.drawText("c", c.x - mTextPaint.measureText("c") - density * 7, c.y + halfOfTextHeight, mTextPaint);
drawIncenterAndExcenter(p1, p2, c, canvas);
}
}
And now finally the layout called canvas_demo.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:triangle="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="2">
<si.kseneman.views.Triangle
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:padding="10dp"
android:rotation="0"
triangle:triangleStrokeColor="@android:color/black"
triangle:triangleColor="#FF33B5E5"
triangle:triangleStrokeWidth="3dp"/>
<si.kseneman.views.TriangleAndArc
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"/>
</LinearLayout>
The final result will look something like this:
You can rotate it by using android:rotation attribute, for example by using 180 it would look something like this
The triangle can of course be smaller in size, it is scalable anyway. You should set it's points a,b,c if this setup doesn't suit you.
NOTE: that i'm not doing any check for arguments, so you will not get any errors if for example you set strokeWidth value that is larger than the view itself. It is your responisibilty to be careful :)
Happy Coding!
来源:https://stackoverflow.com/questions/29492449/android-shape-triangle-with-border