Android shape triangle with frame

I created a triangle shape like in the 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 the triangle border color different from the rest of the shape? If I change the stroke color it works, well I have two sides with different colors, no third border. How do I fix this?

0


source to share


1 answer


Now it took me a while to try and make a good example for you to play with the canvas. The following code can be improved with linear / radial gradients, and you can also make it more customizable if you like. Perhaps I will do it in the future, but I am done for today.

First add this to your resource values. I am using /values/attrs.xml. If you don't 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 the folder where you save your custom views. Correct the package name and import the R class .

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 create a new class (this is not matched, but I did it so you can figure out how to use the 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 a 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 output will look something like this:

Canvas demo1

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

Canvas Demo 2

The triangle can of course be smaller, it scales anyway. You must set its points a, b, c if this setting does not suit you.

NOTE. I don't do any validation for the arguments, so you won't get any errors if, for example, you set the hatch width to a value that is larger than the view itself. This is your answer to be careful :)

Happy coding!

+1


source







All Articles