Как-то довелось мне писать приложение в котором нужно было уметь настраивать круговую диаграмму под свои нужды. Ну в общем я взялся, и начал реализовывать думая что я суперкрутой девелопер, но я не знал как я ошибаюсь. В общем делаю я это все и понимаю что или я это все реализую криво но оно будет работать так как надо, или я это реализую красиво но оно будет работать криво, выбрал я второй вариант, и я пожалел про это… Потому что оно хоть и работает как надо, но все равно криво, а реализация хоть и казалась мне красивой, но спустя несколько лет после этого посмотрев на код я понял что дело дрянь, и даже после рефакторинга все равно красиво не стало, осталось все так же куча непонятных цифр которые мы вычитаем или прибавляем к нашим позициям элементов. Так что если у кого то будут какие-то идеи - то я буду только рад их услышать :)
Я не претендую на нобелевскую премию этим кодом, так что не осуждайте меня :)
Начнем с того что подготовим все для нашего создания этого чудесного 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;
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;
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;
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;
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;
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;
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>
Все. Запускаем и наслаждаемся красотой. Должно выглядеть так как оно отображено на скриншота выше. На идеальный этот код не тянет, возможно в будущем найду время и отрефакторю этот ужас в более красивую форму, а пока имеем что имеем. Надеюсь кому-то пригодится то что я тут навоял.
Исходники:
Комментариев нет:
Отправить комментарий