torsdag 22 november 2012

Android : Creating a custom View (a Circle!)

I've played around with building custom views that can be embedded and styled through XML.
Google has a pretty good tutorial on making them here.

First the requirements, i didn't want to create the View by code and then attach it to a view. Not reusable. I wanted to just be able to type the classname in my layout file and style it as necessary.

First our custom styleable attributes.

values/attrs.xml
<resources>
    <declare-styleable name="circleview">
        <attr name="cRadius" format="integer" />
        <attr name="cFillColor" format="color" />
        <attr name="cStrokeColor" format="color" />
        <attr name="cAngleStart" format="integer" />
        <attr name="cAngleEnd" format="integer" />
    </declare-styleable>
</resources>

As you can probably guess we will have a nice little circle which we can display in a few ways, not so usuable. ( Except to display a Pac-Man ! : )

Lets look at the main layout that incorparate my CircleView class.

layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:circleview="http://schemas.android.com/apk/res/se.adanware.canvasplaying"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">
    <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="Hello World, CanvasActivity"
            />
    <se.adanware.canvasplaying.CircleView
            android:id="@+id/pieCircle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            circleview:cFillColor="#DDaa99"
            circleview:cStrokeColor="@android:color/white"
            circleview:cRadius="80"
            circleview:cAngleStart="30"
            circleview:cAngleEnd="290"
            />
    <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="Hello World, CanvasActivity"
            />
</LinearLayout>

Notice the xmlns:circleview namespace that's called the same as the packagename. The prefix 'circleview' can be whatever we want. I added the two TextViews just to see the span of the height at the beginning.
As you can see we can shape our circle and draw it as we like either put in a radius (in pixels) or we can make use of the height or width. It'll calculate the circle radius so it fits depending on the height or width if the cRadius attribute is omitted. Screenshot of above settings :

Lets move on to the CircleView class. All views that wants to be emedded in xml need to have, quote
'To allow the Android Developer Tools to interact with your view, at a minimum you must provide a constructor that takes a Context and an AttributeSet object as parameters. This constructor allows the layout editor to create and edit an instance of your view.' 
Make sure your constructor has public access, otherwise it's locked to your own package.

src/CircleView.java
package se.adanware.canvasplaying;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class CircleView extends View {

        private Paint circlePaint;
        private Paint circleStrokePaint;
        private RectF circleArc;

        // Attrs
        private int circleRadius;
        private int circleFillColor;
        private int circleStrokeColor;
        private int circleStartAngle;
        private int circleEndAngle;

    public CircleView(Context context, AttributeSet attrs) {

        super(context, attrs);
        init(attrs); // Read all attributes

        circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        circlePaint.setStyle(Paint.Style.FILL);
        circleStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        circleStrokePaint.setStyle(Paint.Style.STROKE);
        circleStrokePaint.setStrokeWidth(2);
        circleStrokePaint.setColor(circleStrokeColor);
    }

    public void init(AttributeSet attrs)
    {
        // Go through all custom attrs.
        TypedArray attrsArray = getContext().obtainStyledAttributes(attrs, R.styleable.circleview);
        circleRadius = attrsArray.getInteger(R.styleable.circleview_cRadius, 0);
        circleFillColor = attrsArray.getColor(R.styleable.circleview_cFillColor, 16777215);
        circleStrokeColor = attrsArray.getColor(R.styleable.circleview_cStrokeColor, -1);
        circleStartAngle = attrsArray.getInteger(R.styleable.circleview_cAngleStart, 0);
        circleEndAngle = attrsArray.getInteger(R.styleable.circleview_cAngleEnd, 360);
        // Google tells us to call recycle.
        attrsArray.recycle();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // Move canvas down and right 1 pixel.
        // Otherwise the stroke gets cut off.
        canvas.translate(1,1);
        circlePaint.setColor(circleFillColor);
        canvas.drawArc(circleArc, circleStartAngle, circleEndAngle, true, circlePaint);
        canvas.drawArc(circleArc, circleStartAngle, circleEndAngle, true, circleStrokePaint);
    }

    @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {

        int measuredWidth = measureWidth(widthMeasureSpec);
        if(circleRadius == 0) // No radius specified.
        {                     // Lets see what we can make.
            // Check width size. Make radius half of available.
            circleRadius = measuredWidth / 2;
            int tempRadiusHeight = measureHeight(heightMeasureSpec) / 2;
            if(tempRadiusHeight < circleRadius)
                // Check height, if height is smaller than
                // width, then go half height as radius.
                circleRadius = tempRadiusHeight;
        }
        // Remove 2 pixels for the stroke.
        int circleDiameter = circleRadius * 2 - 2;
        // RectF(float left, float top, float right, float bottom)
        circleArc = new RectF(0, 0, circleDiameter, circleDiameter);
        int measuredHeight = measureHeight(heightMeasureSpec);
        setMeasuredDimension(measuredWidth, measuredHeight);
        Log.d("onMeasure() ::", "measuredHeight =>" + String.valueOf(measuredHeight) + "px measuredWidth => " + String.valueOf(measuredWidth) + "px");
    }

    private int measureHeight(int measureSpec) {
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        int result = 0;
        if (specMode == MeasureSpec.AT_MOST) {
            result = circleRadius * 2;
        } else if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        }
        return result;
    }

    private int measureWidth(int measureSpec) {
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        int result = 0;
        if (specMode == MeasureSpec.AT_MOST) {
            result = specSize;
        } else if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        }
         return result;
    }
}

As you can see it's really easy. Measure the view and draw accordingly, no failsafe if you specify the radius to large but it's easy to implement though, just check the radius with the width and scale it down. This was just a small example on constructing custom views.

src/CanvasActivity.java
package se.adanware.canvasplaying;

import android.app.Activity;
import android.os.Bundle;

public class CanvasActivity extends Activity {
    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

I'll make another post about adding data to the custom view when i have the time!


3 kommentarer:

  1. Many thanks for the helpful widget.
    How can I interact with the circle using code in runtime to set the color for example?

    SvaraRadera
    Svar
    1. See you solved it, glad it helped!

      Radera
  2. I managed to do this by making the circleFillColor public and assign the new value to it in runtime.

    SvaraRadera