Android shape triangle with border

2019-06-09 11:38发布

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条回答
女痞
2楼-- · 2019-06-09 12:01

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:

Canvas demo1

You can rotate it by using android:rotation attribute, for example by using 180 it would look something like this

Canvas Demo 2

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!

查看更多
登录 后发表回答