суббота, 27 мая 2017 г.

Создаем настраевываемый PieChartView с помощью Canvas

Как-то довелось мне писать приложение в котором нужно было уметь настраивать круговую диаграмму под свои нужды. Ну в общем я взялся, и начал реализовывать думая что я суперкрутой девелопер, но я не знал как я ошибаюсь. В общем делаю я это все и понимаю что или я это все реализую криво но оно будет работать так как надо, или я это реализую красиво но оно будет работать криво, выбрал я второй вариант, и я пожалел про это… Потому что оно хоть и работает как надо, но все равно криво, а реализация хоть и казалась мне красивой, но спустя несколько лет после этого посмотрев на код я понял что дело дрянь, и даже после рефакторинга все равно красиво не стало, осталось все так же куча непонятных цифр которые мы вычитаем или прибавляем к нашим позициям элементов. Так что если у кого то будут какие-то идеи - то я буду только рад их услышать :) 

Я не претендую на нобелевскую премию этим кодом, так что не осуждайте меня :)


Начнем с того что подготовим все для нашего создания этого чудесного pie chart view. Создадим пустой проект в котором у нас будет только SetupPieChartActivity — класс и activity_main — файл разметки. Дальше создаем файл BasePieChartView и начинаем реализацию класса который будет у нас базовым для рисования разных пай чартов. У нас будет два экрана, первый у нас будет для настройки пай чарта, а во втором мы будем отображать то что сделали в первом. Они чуть чуть будут отличаться, но это вы можете видеть на скриншотах ниже:

 
Экран настройки диаграммы

Экран результата на котором отображаем изменения в диаграмме


Все таки начнем. Мы уже создали класс BasePieChartView, давайте же посмотрим как он будет выглядеть.

BasePieChartView.java
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import com.project.chartview.R;
import com.project.chartview.view.helper.CircleRadiusHelper;

/**
 * Created by gleb on 5/27/17.
 */

public abstract class BasePieChartView extends View {

    public Context context;
    private IOnClickListener iOnClickListener;

    public Paint circlePaint;
    public Paint slicePaint;
    public Paint linesPaint;
    public float[] datapoints = {450, 450, 450, 450, 450, 450, 450, 450};
    public float[] sizeOfArcs = new float[360];
    public int[] colorsOfCircles = new int[360];
    public RectF rectf;
    public float centerX;
    public float centerY;

    public float firstPieChartSize = 0;
    public float secondPieChartSize = 0;
    public float thirdPieChartSize = 0;
    public float fourthPieChartSize = 0;
    public float fifthPieChartSize = 0;
    public float sixthPieChartSize = 0;
    public float seventhPieChartSize = 0;
    public float eighthPieChartSize = 0;

    public float cx;
    public float cy;

    public float IXPosition;
    public float IYPosition;

    public float radius;

    public BasePieChartView(Context context) {
        super(context);
    }

    public BasePieChartView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public BasePieChartView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setUpView() {
        slicePaint = new Paint();
        slicePaint.setAntiAlias(true);
        slicePaint.setDither(true);
        slicePaint.setStyle(Paint.Style.FILL);

        linesPaint = new Paint();
        linesPaint.setAntiAlias(true);
        linesPaint.setDither(true);
        linesPaint.setStyle(Paint.Style.FILL);
        linesPaint.setStrokeWidth(2);
        linesPaint.setColor(context.getResources().getColor(android.R.color.white));

        circlePaint = new Paint();
        circlePaint.setAntiAlias(true);
        circlePaint.setDither(true);
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setStrokeWidth(1);

        rectf = new RectF();

        colorsOfCircles[90] = R.color.circle_first_color;
        colorsOfCircles[135] = R.color.circle_second_color;
        colorsOfCircles[180] = R.color.circle_third_color;
        colorsOfCircles[225] = R.color.circle_forth_color;
        colorsOfCircles[270] = R.color.circle_fifth_color;
        colorsOfCircles[270] = R.color.circle_sixth_color;
        colorsOfCircles[0] = R.color.circle_seventh_color;
        colorsOfCircles[45] = R.color.circle_eight_color;
    }

    public void drawingSettings() {
        int startTop = 0;
        int startLeft = 0;
        int endBottom = getHeight();
        int endRight = getWidth();

        rectf.set(startLeft, startTop, endRight, endBottom);

        centerX = rectf.centerX();
        centerY = rectf.centerY();

        cx = getWidth() / 2f;
        cy = getHeight() / 2f;
    }

    public void drawBackGroundCircle(Canvas canvas) {
        slicePaint.setColor(context.getResources().getColor(R.color.round_color_center_circle));
        canvas.drawCircle(centerX, centerY, radius, slicePaint);

        radius = CircleRadiusHelper.getRadius(canvas);
    }

    public void drawNineCircles(Canvas canvas) {
        circlePaint.setColor(context.getResources().getColor(R.color.white));

        int circlesCount = 10;
        float percent = 0f;
        float percentStep = 1f / (float) circlesCount;

        for (int i = 0; i < circlesCount; i++) {
            percent += percentStep;
            canvas.drawCircle(centerX, centerY, getCircleRadius(percent), circlePaint);
        }
    }

    public void drawPartsOfPie(Canvas canvas) {
        float[] scaledValues = scale();
        float sliceStartPoint = 0;

        calculateRectForPieSlice(rectf, firstPieChartSize);
        slicePaint.setColor(context.getResources().getColor(R.color.circle_first_color));
        canvas.drawArc(rectf, sliceStartPoint, scaledValues[0], true, slicePaint);
        sliceStartPoint += scaledValues[0];

        calculateRectForPieSlice(rectf, secondPieChartSize);
        slicePaint.setColor(context.getResources().getColor(R.color.circle_second_color));
        canvas.drawArc(rectf, sliceStartPoint, scaledValues[1], true, slicePaint);
        sliceStartPoint += scaledValues[1];

        calculateRectForPieSlice(rectf, thirdPieChartSize);
        slicePaint.setColor(context.getResources().getColor(R.color.circle_third_color));
        canvas.drawArc(rectf, sliceStartPoint, scaledValues[2], true, slicePaint);
        sliceStartPoint += scaledValues[2];

        calculateRectForPieSlice(rectf, fourthPieChartSize);
        slicePaint.setColor(context.getResources().getColor(R.color.circle_forth_color));
        canvas.drawArc(rectf, sliceStartPoint, scaledValues[3], true, slicePaint);
        sliceStartPoint += scaledValues[3];

        calculateRectForPieSlice(rectf, fifthPieChartSize);
        slicePaint.setColor(context.getResources().getColor(R.color.circle_fifth_color));
        canvas.drawArc(rectf, sliceStartPoint, scaledValues[4], true, slicePaint);
        sliceStartPoint += scaledValues[4];

        calculateRectForPieSlice(rectf, sixthPieChartSize);
        slicePaint.setColor(context.getResources().getColor(R.color.circle_sixth_color));
        canvas.drawArc(rectf, sliceStartPoint, scaledValues[5], true, slicePaint);
        sliceStartPoint += scaledValues[5];

        calculateRectForPieSlice(rectf, seventhPieChartSize);
        slicePaint.setColor(context.getResources().getColor(R.color.circle_seventh_color));
        canvas.drawArc(rectf, sliceStartPoint, scaledValues[6], true, slicePaint);
        sliceStartPoint += scaledValues[6];

        calculateRectForPieSlice(rectf, eighthPieChartSize);
        slicePaint.setColor(context.getResources().getColor(R.color.circle_eight_color));
        canvas.drawArc(rectf, sliceStartPoint, scaledValues[7], true, slicePaint);
    }

    public void drawStrokeBackgroundLines(Canvas canvas) {
        float scaleMarkSize = getResources().getDisplayMetrics().density * 16;
        float radius = Math.min(getWidth(), getHeight());

        for (int i = 0; i < 360; i += 45) {
            float angle = (float) (i * Math.PI / 180f);

            float stopX = (float) (centerX + (radius - scaleMarkSize) * Math.sin(angle));
            float stopY = (float) (centerY - (radius - scaleMarkSize) * Math.cos(angle));

            canvas.drawLine(centerX, centerY, stopX, stopY, linesPaint);
        }
    }

    public void drawCircleWithI(Canvas canvas) {
        slicePaint.setColor(context.getResources().getColor(R.color.round_color_center_circle_second));
        canvas.drawCircle(centerX, centerY, radius / 4, slicePaint);
        slicePaint.setColor(context.getResources().getColor(R.color.round_color_center_circle));
        canvas.drawCircle(centerX, centerY, radius / 6, slicePaint);
    }

    public void drawI(Canvas canvas) {
        slicePaint.setTextSize(60);
        slicePaint.setTextAlign(Paint.Align.CENTER);
        slicePaint.setColor(context.getResources().getColor(R.color.round_color_center_circle_text));
        canvas.drawText("i", centerX, centerY + 20, slicePaint);
        IXPosition = centerX;
        IYPosition = centerY;
    }

    public float[] scale() {
        float[] scaledValues = new float[this.datapoints.length];
        float total = getTotal();
        for (int i = 0; i < this.datapoints.length; i++) {
            scaledValues[i] = (this.datapoints[i] / total) * 360;
        }
        return scaledValues;
    }

    private float getCircleRadius(float percent) {
        float invisibleRadius = radius / 4;
        float visibleRadius = radius - invisibleRadius;
        return invisibleRadius + visibleRadius * percent;
    }

    private void calculateRectForPieSlice(RectF source, float slicePercent) {
        float invisibleRadius = radius / 4;
        float visibleRadius = radius - invisibleRadius;
        float sliceRadius = invisibleRadius + visibleRadius * slicePercent;

        source.set(centerX - sliceRadius, centerY - sliceRadius, centerX + sliceRadius, centerY + sliceRadius);
    }

    public float getTotal() {
        float total = 0;
        for (float val : this.datapoints)
            total += val;
        return total;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                final float x = ev.getX();
                final float y = ev.getY();
                if(x >= IXPosition - 50 && y >= IYPosition - 50 && x <= IXPosition + 50 && y <= IYPosition + 50)
                    iOnClickListener.iWasClicked();
                break;
        }
        return true;
    }

    public void setOnIClickListener(IOnClickListener listener) {
        this.iOnClickListener = listener;
    }

    public interface IOnClickListener {
        void iWasClicked();
    }

    public void setFirstPieChartSize(float firstPieChartSize) {
        sizeOfArcs[90] = firstPieChartSize;
        this.firstPieChartSize = firstPieChartSize;
    }

    public void setSecondPieChartSize(float secondPieChartSize) {
        sizeOfArcs[135] = secondPieChartSize;
        this.secondPieChartSize = secondPieChartSize;
    }

    public void setThirdPieChartSize(float thirdPieChartSize) {
        sizeOfArcs[180] = thirdPieChartSize;
        this.thirdPieChartSize = thirdPieChartSize;
    }

    public void setFourthPieChartSize(float fourthPieChartSize) {
        sizeOfArcs[225] = fourthPieChartSize;
        this.fourthPieChartSize = fourthPieChartSize;
    }

    public void setFifthPieChartSize(float fifthPieChartSize) {
        sizeOfArcs[270] = fifthPieChartSize;
        this.fifthPieChartSize = fifthPieChartSize;
    }

    public void setSixthPieChartSize(float sixthPieChartSize) {
        sizeOfArcs[315] = sixthPieChartSize;
        this.sixthPieChartSize = sixthPieChartSize;
    }

    public void setSeventhPieChartSize(float seventhPieChartSize) {
        sizeOfArcs[0] = seventhPieChartSize;
        this.seventhPieChartSize = seventhPieChartSize;
    }

    public void setEighthPieChartSize(float sevenPieChartSize) {
        sizeOfArcs[45] = sevenPieChartSize;
        this.eighthPieChartSize = sevenPieChartSize;
    }
}


Давайте я в кратце опишу что тут да как, комментарии я не буду в этой статье в коде писать ибо мне впадлу описывать все детали. Я чисто в общих чертах опишу что, где и зачем. Отрисовка у нас происходит для каждого слоя разная, так например мы фон рисуем в одном методе, линии в другом, пай чарты в третем и т.д. Иначе будет каша, а не код, поверьте, я пробовал отрефакторить это…

Значит для начала у нас есть метод setUpView, его мы сделали для того что бы при первом запуске у нас инициализировались все нужные переменные, тут мы задаем толщину линий, цвет и т. д. Эти переменные мы будем дальше использовать для отрисовки элементов на экране.

Дальше идет метод drawingSettings, в нем мы задаем параметры для отображения элементов на экране, вычисляем центр и задаем точки центра в переменные centerX и centerY.

Дальше рисуем фоновый круг серого цвета в методе drawBackGroundCircle, очевидно же. Просто задаем цвет и рисуем круг четко по центру экрана во весь размер вьюхи, и вычисляем радиус который мы можем себе позволить.

Дальше в методе drawNineCircles мы рисуем 9 линий которые нам нужны для разделения на 10 секций нашего круга. Опять же, задаем цвет, расчитываем радиус кругов, и рисуем их на экране белым цветом.

Дальше нам нужно нарисовать под нашим кругом наши части пай чарта, это все мы делаем в методе drawPartsOfPie, там у нас некрасиво повторяется один и тот же код с разными параметрами, собственно опять, вычисляем насколько мы можем позволить себе вылазить за границы, дальше задаем цвет кусочка, рисуем кусочек, и потом задаем его стартовую позицию на канвасе, дальше он у нас будет расти в зависимости от заданого параметра с помощью SeekBar'a. Будем обновлять вьюху с новыми параметрами.

Потом идет следующий метод который рисует у нас линии разделяющие наши пай чарты на секции, вот этот метод drawStrokeBackgroundLines. Там мы вычисляем позволяемую длинну на которую нам нужно эти линии нарисовать, что бы небыло такого что у нас они будут недостаточной длинны, находим центр и от него начинаем в разные стороны рисовать с отступом в 45 градусов по кругу.

Дальше нам нужно нарисовать круг на котором у нас будет кнопка I, по тыку на нее задумывалось выводить какой-то текст который будет описывать работу этой диаграммы. Метод называется drawCircleWithI. Тут все просто, задаем цвет, рисуем круг побольше, потому снова меняем цвет и рисуем круг поменьше.

Дальше рисуем на кругу нашу букву I. Метод drawI. Просто задаем цвет текста, высоту размер, где она должна быть и рисуем ее. Запоминаем позицию этой буквы, дальше нам она понадобится.

Потом у нас идут методы вычисления центра, размера холста, расчитывание размера кусочка и т.д. 

Клик по кнопке I у нас находится в onTouchEvent методе. Там мы берем переменные центра экрана и не хитрым if'ом проверяем тыкнули туда или нет, если тыкнули то вызываем колбек.

Дальше идут сеттеры для перерисовки кусочков пай чарта, ну и сам интерфейс клика по I.

Скорей всего у вас там щас частично код подсвечивается крассным. Это потому что не хватает некоторых цветов и атрибутов и классов. Вот они ниже:

color.xml
    <color name="white">#FFFFFF</color>

    <color name="circle.first.color">#16cdd5</color>
    <color name="circle.second.color">#ffdf00</color>
    <color name="circle.third.color">#b76cad</color>
    <color name="circle.forth.color">#237ac1</color>
    <color name="circle.fifth.color">#5a9f1f</color>
    <color name="circle.sixth.color">#ff9600</color>
    <color name="circle.seventh.color">#fa31ad</color>
    <color name="circle.eight.color">#f63d1f</color>

    <color name="round.color.center.circle">#dddddd</color>
    <color name="round.color.center.circle.second">#f2f2f2</color>
    <color name="round.color.center.circle.text">#4d4d4d</color>


dimens.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="myFontSize">16sp</dimen>
</resources>


strings.xml
    <string name="circle.description">Slide the seekbars to change the chart view status</string>
    <string name="circle.preview.description">The result of your setup for pie charts</string>
    <string name="circle.first">First</string>
    <string name="circle.second">Second</string>
    <string name="circle.third">Third</string>
    <string name="circle.forth">Fourth</string>
    <string name="circle.fifth">Fifth</string>
    <string name="circle.sixth">Sixth</string>
    <string name="circle.seventh">Seventh</string>
    <string name="circle.eighth">Eighth</string>

    <string name="button.ready">Send</string>

    <string name="button.i.clicked">You clicked on center of the chart!</string>


И класс для вычисления радиуса холста:

CircleRadiusHelper.java
import android.graphics.Canvas;

/**
 * Created by gleb on 5/27/17.
 */

public class CircleRadiusHelper {
    public static float getRadius(Canvas canvas) {
        float width = canvas.getWidth();
        float height = canvas.getHeight();
        float minSize = width > height ? height : width;
        float radius = minSize / 2;
        return radius;
    }
}


Теперь думаю хватает все что требуется для дальнейшей работы.
Собственно вот такой у нас пока класс для отрисовки пай чарта, но это не все, это только базовый класс, в котором присутствуют все нужые нам методы, дальше нам нужно их собрать до кучи в нашем классе который мы собственно будем использовать в активити для вывода пай чарта.

Создаем класс PieChartView и вставляем код который ниже.

PieChartView
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;

/**
 * Created by gleb on 5/27/17.
 */

public class PieChartView extends BasePieChartView {

    public PieChartView(Context context) {
        super(context);
        this.context = context;
        setUpView();
    }

    public PieChartView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        setUpView();
    }

    public PieChartView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        setUpView();
    }

    @Override
    public void onDraw(Canvas canvas) {
        drawingSettings();
        drawBackGroundCircle(canvas);
        drawPartsOfPie(canvas);
        drawNineCircles(canvas);
        drawStrokeBackgroundLines(canvas);
        drawCircleWithI(canvas);
        drawI(canvas);
    }
}


Тут как вы видите, мы вызываем setUpView в конструкторе для инициализации всех переменных и в onDraw вызываем нужные нам методы для отрисовки пай чарта. Все красивенько пока :)

Дальше нам нужно вставить наш пай чарт в activity_main, там у нас будет помимо пай чарта — 8 SeekBar'ов которыми мы будем настраивать его, так что XML получится очень длинный…

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:orientation="vertical">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/scrollView" >

        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <LinearLayout
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:padding="10dp">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:textAppearance="?android:attr/textAppearanceMedium"
                    android:text="@string/circle.description"
                    android:id="@+id/description"
                    android:textColor="@android:color/black" />

                <com.project.chartview.view.PieChartView
                    android:layout_width="match_parent"
                    android:layout_height="350dp"
                    android:id="@+id/round"
                    android:layout_marginTop="10dp" />

                <LinearLayout
                    android:orientation="vertical"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_below="@+id/round">

                    <LinearLayout
                        android:orientation="vertical"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_marginTop="20dp"
                        android:id="@+id/firstSettingView">

                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:textAppearance="?android:attr/textAppearanceLarge"
                            android:text="@string/circle.first"
                            android:id="@+id/textView7" />

                        <LinearLayout
                            android:orientation="horizontal"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:gravity="center_vertical">

                            <SeekBar
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:id="@+id/firstSeekBar"
                                android:layout_below="@+id/round"
                                android:layout_centerHorizontal="true"
                                android:layout_weight="0.1" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="0"
                                android:id="@+id/firstPercent"
                                android:textColor="@android:color/black"
                                android:backgroundTint="@android:color/transparent"
                                android:inputType="numberDecimal" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="%"
                                android:id="@+id/textView56"
                                android:textColor="@android:color/black" />
                        </LinearLayout>

                    </LinearLayout>

                    <LinearLayout
                        android:orientation="vertical"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_marginTop="20dp"
                        android:id="@+id/secondSettingView">

                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:textAppearance="?android:attr/textAppearanceLarge"
                            android:text="@string/circle.second"
                            android:id="@+id/textView8" />

                        <LinearLayout
                            android:orientation="horizontal"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:gravity="center_vertical">

                            <SeekBar
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:id="@+id/secondSeekBar"
                                android:layout_below="@+id/round"
                                android:layout_centerHorizontal="true"
                                android:layout_weight="0.1" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="0"
                                android:id="@+id/secondPercent"
                                android:textColor="@android:color/black"
                                android:backgroundTint="@android:color/transparent"
                                android:inputType="numberDecimal" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="%"
                                android:id="@+id/textView44"
                                android:textColor="@android:color/black" />
                        </LinearLayout>

                    </LinearLayout>

                    <LinearLayout
                        android:orientation="vertical"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_marginTop="20dp"
                        android:id="@+id/thirdSettingView">

                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:textAppearance="?android:attr/textAppearanceLarge"
                            android:text="@string/circle.third"
                            android:id="@+id/textView6" />

                        <LinearLayout
                            android:orientation="horizontal"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:gravity="center_vertical">

                            <SeekBar
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:id="@+id/thirdSeekBar"
                                android:layout_below="@+id/round"
                                android:layout_centerHorizontal="true"
                                android:layout_weight="0.1" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="0"
                                android:id="@+id/thirdPercent"
                                android:textColor="@android:color/black"
                                android:backgroundTint="@android:color/transparent"
                                android:inputType="numberDecimal" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="%"
                                android:id="@+id/textView54"
                                android:textColor="@android:color/black" />
                        </LinearLayout>

                    </LinearLayout>

                    <LinearLayout
                        android:orientation="vertical"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_marginTop="20dp"
                        android:id="@+id/forthSettingView">

                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:textAppearance="?android:attr/textAppearanceLarge"
                            android:text="@string/circle.forth"
                            android:id="@+id/textView5" />

                        <LinearLayout
                            android:orientation="horizontal"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:gravity="center_vertical">

                            <SeekBar
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:id="@+id/forthSeekBar"
                                android:layout_below="@+id/round"
                                android:layout_centerHorizontal="true"
                                android:layout_weight="0.1" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="0"
                                android:id="@+id/forthPercent"
                                android:textColor="@android:color/black"
                                android:backgroundTint="@android:color/transparent"
                                android:inputType="numberDecimal" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="%"
                                android:id="@+id/textView55"
                                android:textColor="@android:color/black" />
                        </LinearLayout>

                    </LinearLayout>

                    <LinearLayout
                        android:orientation="vertical"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_marginTop="20dp"
                        android:id="@+id/fifthSettingView">

                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:textAppearance="?android:attr/textAppearanceLarge"
                            android:text="@string/circle.fifth"
                            android:id="@+id/textView4" />

                        <LinearLayout
                            android:orientation="horizontal"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:gravity="center_vertical">

                            <SeekBar
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:id="@+id/fifthSeekBar"
                                android:layout_below="@+id/round"
                                android:layout_centerHorizontal="true"
                                android:layout_weight="0.1" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="0"
                                android:id="@+id/fifthPercent"
                                android:textColor="@android:color/black"
                                android:backgroundTint="@android:color/transparent"
                                android:inputType="numberDecimal" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="%"
                                android:id="@+id/textView57"
                                android:textColor="@android:color/black" />
                        </LinearLayout>

                    </LinearLayout>

                    <LinearLayout
                        android:orientation="vertical"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_marginTop="20dp"
                        android:id="@+id/sixthSettingView">

                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:textAppearance="?android:attr/textAppearanceLarge"
                            android:text="@string/circle.sixth"
                            android:id="@+id/textView3" />

                        <LinearLayout
                            android:orientation="horizontal"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:gravity="center_vertical">

                            <SeekBar
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:id="@+id/sixthSeekBar"
                                android:layout_below="@+id/round"
                                android:layout_centerHorizontal="true"
                                android:layout_weight="0.1" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="0"
                                android:id="@+id/sixthPercent"
                                android:textColor="@android:color/black"
                                android:backgroundTint="@android:color/transparent"
                                android:inputType="numberDecimal" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="%"
                                android:id="@+id/textView58"
                                android:textColor="@android:color/black" />

                        </LinearLayout>

                    </LinearLayout>

                    <LinearLayout
                        android:orientation="vertical"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_marginTop="20dp"
                        android:id="@+id/seventhSettingView">

                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:textAppearance="?android:attr/textAppearanceLarge"
                            android:text="@string/circle.seventh"
                            android:id="@+id/textView2" />

                        <LinearLayout
                            android:orientation="horizontal"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:gravity="center_vertical">

                            <SeekBar
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:id="@+id/seventhSeekBar"
                                android:layout_below="@+id/round"
                                android:layout_centerHorizontal="true"
                                android:layout_weight="0.1" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="0"
                                android:id="@+id/seventhPercent"
                                android:textColor="@android:color/black"
                                android:backgroundTint="@android:color/transparent"
                                android:inputType="numberDecimal" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="%"
                                android:id="@+id/textView60"
                                android:textColor="@android:color/black" />
                        </LinearLayout>

                    </LinearLayout>

                    <LinearLayout
                        android:orientation="vertical"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_marginTop="20dp"
                        android:id="@+id/eighthSettingView">

                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:textAppearance="?android:attr/textAppearanceLarge"
                            android:text="@string/circle.eighth"
                            android:id="@+id/textView" />

                        <LinearLayout
                            android:orientation="horizontal"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:gravity="center_vertical">

                            <SeekBar
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:id="@+id/eighthSeekBar"
                                android:layout_below="@+id/round"
                                android:layout_centerHorizontal="true"
                                android:layout_weight="0.1" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="0"
                                android:id="@+id/eighthPercent"
                                android:textColor="@android:color/black"
                                android:backgroundTint="@android:color/transparent"
                                android:inputType="numberDecimal" />

                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textAppearance="?android:attr/textAppearanceMedium"
                                android:text="%"
                                android:id="@+id/textView61"
                                android:textColor="@android:color/black" />
                        </LinearLayout>

                    </LinearLayout>

                </LinearLayout>

                <Button
                    android:layout_width="150dp"
                    android:layout_height="wrap_content"
                    android:text="@string/button.ready"
                    android:id="@+id/button"
                    android:layout_gravity="center_horizontal"
                    android:textColor="@color/white"
                    android:background="@color/colorPrimary"
                    android:layout_marginTop="20dp"
                    android:layout_marginBottom="20dp" />
            </LinearLayout>

        </LinearLayout>
    </ScrollView>

</LinearLayout>


Думаю можно было бы сделать это все в RecycleView, и как-то это красиво реализовать, но почему то не получилось сходу так сделать, пришлось создавать такой вот ужас.

Тут думаю все понятно, мы добавили пай чарт и кучу сикбаров с текствьюхами для отображения прогресса и кнопкой вконце экрана что бы по нажатию на нее переходить на следующий экран с результатом.

SetupPieChartActivity же в свою очередь будет в себе содержать реализацию всего этого. 

SetupPieChartActivity.java
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;

import com.project.chartview.etc.SharedPrefs;
import com.project.chartview.view.BasePieChartView;
import com.project.chartview.view.PieChartView;

import butterknife.BindView;
import butterknife.ButterKnife;

import static com.project.chartview.view.PreviewPieChartView.ARC_SIZE;

public class SetupPieChartActivity extends BaseChartViewActivity implements SeekBar.OnSeekBarChangeListener,
        View.OnClickListener, BasePieChartView.IOnClickListener {

    @BindView(R.id.firstSeekBar)
    SeekBar firstSeekBar;
    @BindView(R.id.secondSeekBar)
    SeekBar secondSeekBar;
    @BindView(R.id.thirdSeekBar)
    SeekBar thirdSeekBar;
    @BindView(R.id.forthSeekBar)
    SeekBar forthSeekBar;
    @BindView(R.id.fifthSeekBar)
    SeekBar fifthSeekBar;
    @BindView(R.id.sixthSeekBar)
    SeekBar sixthSeekBar;
    @BindView(R.id.seventhSeekBar)
    SeekBar seventhSeekBar;
    @BindView(R.id.eighthSeekBar)
    SeekBar eighthSeekBar;

    @BindView(R.id.firstPercent)
    TextView firstPercent;
    @BindView(R.id.secondPercent)
    TextView secondPercent;
    @BindView(R.id.thirdPercent)
    TextView thirdPercent;
    @BindView(R.id.forthPercent)
    TextView forthPercent;
    @BindView(R.id.fifthPercent)
    TextView fifthPercent;
    @BindView(R.id.sixthPercent)
    TextView sixthPercent;
    @BindView(R.id.seventhPercent)
    TextView seventhPercent;
    @BindView(R.id.eighthPercent)
    TextView eighthPercent;

    @BindView(R.id.round)
    PieChartView pieChartView;

    @BindView(R.id.button)
    Button readyButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        firstSeekBar.setOnSeekBarChangeListener(this);
        secondSeekBar.setOnSeekBarChangeListener(this);
        thirdSeekBar.setOnSeekBarChangeListener(this);
        forthSeekBar.setOnSeekBarChangeListener(this);
        fifthSeekBar.setOnSeekBarChangeListener(this);
        sixthSeekBar.setOnSeekBarChangeListener(this);
        seventhSeekBar.setOnSeekBarChangeListener(this);
        eighthSeekBar.setOnSeekBarChangeListener(this);
        readyButton.setOnClickListener(this);
        pieChartView.setOnIClickListener(this);
    }

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean b) {
        switch (seekBar.getId()) {
            case R.id.firstSeekBar:
                pieChartView.setFirstPieChartSize(progress / ARC_SIZE);
                firstPercent.setText(String.valueOf(progress));
                break;
            case R.id.secondSeekBar:
                pieChartView.setSecondPieChartSize(progress / ARC_SIZE);
                secondPercent.setText(String.valueOf(progress));
                break;
            case R.id.thirdSeekBar:
                pieChartView.setThirdPieChartSize(progress / ARC_SIZE);
                thirdPercent.setText(String.valueOf(progress));
                break;
            case R.id.forthSeekBar:
                pieChartView.setFourthPieChartSize(progress / ARC_SIZE);
                forthPercent.setText(String.valueOf(progress));
                break;
            case R.id.fifthSeekBar:
                pieChartView.setFifthPieChartSize(progress / ARC_SIZE);
                fifthPercent.setText(String.valueOf(progress));
                break;
            case R.id.sixthSeekBar:
                pieChartView.setSixthPieChartSize(progress / ARC_SIZE);
                sixthPercent.setText(String.valueOf(progress));
                break;
            case R.id.seventhSeekBar:
                pieChartView.setSeventhPieChartSize(progress / ARC_SIZE);
                seventhPercent.setText(String.valueOf(progress));
                break;
            case R.id.eighthSeekBar:
                pieChartView.setEighthPieChartSize(progress / ARC_SIZE);
                eighthPercent.setText(String.valueOf(progress));
                break;
        }
        pieChartView.invalidate();
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) { }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) { }

    @Override
    public void onClick(View view) {
        int[] charts = new int[8];
        charts[0] = firstSeekBar.getProgress();
        charts[1] = secondSeekBar.getProgress();
        charts[2] = thirdSeekBar.getProgress();
        charts[3] = forthSeekBar.getProgress();
        charts[4] = fifthSeekBar.getProgress();
        charts[5] = sixthSeekBar.getProgress();
        charts[6] = seventhSeekBar.getProgress();
        charts[7] = eighthSeekBar.getProgress();
        SharedPrefs.setPieCharts(getApplicationContext(), charts);

        startActivity(new Intent(this, PreviewPieChartActivity.class));
    }

    @Override
    public void iWasClicked() {
        Toast.makeText(this, getString(R.string.button_i_clicked), Toast.LENGTH_LONG).show();
    }
}


Тут мы забиндили все вьюхи для того что бы ими рулить. Это мы сделали при помощи библиотеки ButterKnife, если кто не знал — то она очень удобная! Возвращаясь к нашему коду, в onCreate мы проинициализировали все лисенеры которые нам нужны, добавились методы для отслеживания изменения статуса SeekBar'ов — onProgressChanged. В них мы отслеживаем что-где меняется, и отправляем это в нашу вьюу, дальше вызываем pieChartView.invalidate() для обновления самой вьюхи, без этого метода у нас на экране ничего не поменяется…

BaseChartViewActivity — это пустой класс унаследованный от AppCompatActivity, его я создал с целью вынести туда повторяющийся функционал, но пока не задалось, так что можете или создать такой же класс или унаследовать просто от AppCompatActivity.

Дальше мы сохраняем в Shared preferences изменения которые мы ввели. Класс кстати вот:

SharedPrefs.java
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;

/**
 * Created by gleb on 5/27/17.
 */

public class SharedPrefs {

    private static final String FIRST_PIE = "firstPie";
    private static final String SECOND_PIE = "secondPie";
    private static final String THIRD_PIE = "thirdPie";
    private static final String FORTH_PIE = "forthPie";
    private static final String FIFTH_PIE = "fifthPie";
    private static final String SIXTH_PIE = "sixthPie";
    private static final String SEVENTH_PIE = "seventhPie";
    private static final String EIGHTH_PIE = "eigthPie";

    public static void setPieCharts(Context context, int[] val) {
        PreferenceManager.getDefaultSharedPreferences(context)
                .edit()
                .putInt(FIRST_PIE, val[0])
                .putInt(SECOND_PIE, val[1])
                .putInt(THIRD_PIE, val[2])
                .putInt(FORTH_PIE, val[3])
                .putInt(FIFTH_PIE, val[4])
                .putInt(SIXTH_PIE, val[5])
                .putInt(SEVENTH_PIE, val[6])
                .putInt(EIGHTH_PIE, val[7])
                .commit();
    }

    public static int[] getPieCharts(Context context) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        int[] pieCharts = new int[8];
        pieCharts[0] = preferences.getInt(FIRST_PIE, 0);
        pieCharts[1] = preferences.getInt(SECOND_PIE, 0);
        pieCharts[2] = preferences.getInt(THIRD_PIE, 0);
        pieCharts[3] = preferences.getInt(FORTH_PIE, 0);
        pieCharts[4] = preferences.getInt(FIFTH_PIE, 0);
        pieCharts[5] = preferences.getInt(SIXTH_PIE, 0);
        pieCharts[6] = preferences.getInt(SEVENTH_PIE, 0);
        pieCharts[7] = preferences.getInt(EIGHTH_PIE, 0);
        return pieCharts;
    }
}


Тут всего два метода, в один мы сетим данные, а со второго вытягиваем. Все предельно просто, я это сделал что бы не передавать полотна текста через интент, так проще, и всегда под рукой.

Запускаем смотрим, должно заработать и позволять покрутить сикбары и соответственно изменять состояние пай чарта. Если не завелось проверьте может что-то забыли. 

Дальше переходим ко второму экрану. На нем у нас будет всего лишь одна текствьюха и PreviewChartView, который будет отображать то что мы сделали на первом экране, собственно это видно на скриншоте выше.

Сперва создадим новую вьюху, называться она будет PreviewPieChartView. В ней мы сделаем все тоже самое что было в предыдущей, только добавим небольшой функционал который будет добавлять кружки с введенным результатом, их мы нарисуем прямо в onDraw самой вьюхи.

PreviewChartView.java
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;

import com.project.chartview.R;

/**
 * Created by gleb on 5/27/17.
 */

public class PreviewPieChartView extends BasePieChartView {

    public static final float ARC_SIZE = 100f;

    public PreviewPieChartView(Context context) {
        super(context);
        this.context = context;
        setUpView();
    }

    public PreviewPieChartView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        setUpView();
    }

    public PreviewPieChartView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        setUpView();
    }

    @Override
    public void onDraw(Canvas canvas) {
        drawingSettings();

        drawBackGroundCircle(canvas);
        drawNineCircles(canvas);
        drawPartsOfPie(canvas);
        drawStrokeBackgroundLines(canvas);
        drawCircleWithI(canvas);
        drawI(canvas);

        int scaledSize = getResources().getDimensionPixelSize(R.dimen.myFontSize);
        float circleRadius = radius;
        for (int i = 0; i < 360; i += 45) {
            float angle = (float) (i * Math.PI / 180f) - 75;

            float startX = (float) (cx + circleRadius * Math.sin(angle));
            float startY = (float) (cy - circleRadius * Math.cos(angle));

            slicePaint.setColor(context.getResources().getColor(android.R.color.white));
            canvas.drawCircle(startX, startY, radius / 10, slicePaint);

            if(colorsOfCircles[i] != 0)
                circlePaint.setColor(context.getResources().getColor(colorsOfCircles[i]));
            canvas.drawCircle(startX, startY, radius / 10, circlePaint);

            slicePaint.setTextAlign(Paint.Align.CENTER);
            slicePaint.setTextSize(scaledSize);
            slicePaint.setColor(context.getResources().getColor(R.color.round_color_center_circle_text));
            canvas.drawText(String.valueOf((int)(sizeOfArcs[i] * ARC_SIZE)), startX, startY + 20, slicePaint);
        }
    }
}


Тут у нас в коде мы делаем тоже самое что мы делали в предыдущей вьюхе, только теперь мы добавляем по тому же принципу что мы добавляли линии — круги с текстом, но так как мы хотим что бы круги были не на линиях мы там делаем отступ на 75 пикселей, что бы они были ближе к центру кусочка нашего пай чарта. Вот собственно и вся разница между этими двумя пай чартами.

Теперь мы хотим его добавить в xml. Создаем activity_preview и пишем туда следующую разметку.

activity_preview.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:id="@+id/textView9"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="@string/circle.preview.description"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:textColor="@android:color/black" />

    <com.project.chartview.view.PreviewPieChartView
        android:id="@+id/preview"
        android:layout_width="match_parent"
        android:layout_height="350dp"
        android:layout_marginTop="10dp"
        android:background="@color/white"
        android:padding="10dp" />

</LinearLayout>


И дальше в PreviewPieChartActivity реализовываем работу этого чарта.

PreviewPieChartActivity.java
import com.project.chartview.etc.SharedPrefs;
import com.project.chartview.view.BasePieChartView;
import com.project.chartview.view.PreviewPieChartView;

import butterknife.BindView;
import butterknife.ButterKnife;

import static com.project.chartview.view.PreviewPieChartView.ARC_SIZE;

/**
 * Created by gleb on 5/27/17.
 */

public class PreviewPieChartActivity extends BaseChartViewActivity implements BasePieChartView.IOnClickListener {

    @BindView(R.id.preview)
    PreviewPieChartView previewPieChartView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_preview);
        ButterKnife.bind(this);

        previewPieChartView.setOnIClickListener(this);
        previewPieChartView.setFirstPieChartSize(SharedPrefs.getPieCharts(this)[0] / ARC_SIZE);
        previewPieChartView.setSecondPieChartSize(SharedPrefs.getPieCharts(this)[1] / ARC_SIZE);
        previewPieChartView.setThirdPieChartSize(SharedPrefs.getPieCharts(this)[2] / ARC_SIZE);
        previewPieChartView.setFourthPieChartSize(SharedPrefs.getPieCharts(this)[3] / ARC_SIZE);
        previewPieChartView.setFifthPieChartSize(SharedPrefs.getPieCharts(this)[4] / ARC_SIZE);
        previewPieChartView.setSixthPieChartSize(SharedPrefs.getPieCharts(this)[5] / ARC_SIZE);
        previewPieChartView.setSeventhPieChartSize(SharedPrefs.getPieCharts(this)[6] / ARC_SIZE);
        previewPieChartView.setEighthPieChartSize(SharedPrefs.getPieCharts(this)[7] / ARC_SIZE);
        previewPieChartView.invalidate();
    }

    @Override
    public void iWasClicked() {
        Toast.makeText(this, getString(R.string.button_i_clicked), Toast.LENGTH_LONG).show();
    }
}


Незабываем добавить этот класс в манифест, а то по клику будет падать с ошибкой что данный класс не объявлен в манифесте. 
У меня он выглядит так:

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.project.chartview">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".SetupPieChartActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name=".PreviewPieChartActivity" />
    </application>

</manifest>


Все. Запускаем и наслаждаемся красотой. Должно выглядеть так как оно отображено на скриншота выше. На идеальный этот код не тянет, возможно в будущем найду время и отрефакторю этот ужас в более красивую форму, а пока имеем что имеем. Надеюсь кому-то пригодится то что я тут навоял.


Исходники:

GitHub

Комментариев нет:

Отправить комментарий