Как на андроид построить гистограмму
Перейти к содержимому

Как на андроид построить гистограмму

  • автор:

Создание диаграммы в Excel на мобильных устройствах

Воспользуйтесь командой Рекомендуемые диаграммы на вкладке Вставка для быстрого создания диаграммы, идеально соответствующей вашим данным. Кроме того, вы можете создать собственную диаграмму.

Маркер

Создание диаграммы на телефоне или планшете с Android

  1. Откройте книгу и перейдите в электронную таблицу, содержащую данные.
  2. Выберите данные, которые необходимо нанести на диаграмму, перетащив маркеры

Вкладка

.
На планшете с Android нажмите Вставка.

Значок

Если у вас телефон с Android, коснитесь значка редактирования

Выбор диаграммы на вкладке

Советы:

  • Если подходящего формата нет, нажмите Диаграмма на вкладке Вставка, чтобы просмотреть все доступные типы диаграмм.

Вкладка

При выборе макета диаграммы появляется вкладка Диаграмма.

Контекстное меню

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

Маркер

Создание диаграммы на iPhone или iPad

  1. Откройте книгу и перейдите в электронную таблицу, содержащую данные.
  2. Выберите данные, которые необходимо нанести на диаграмму, перетащив маркеры

Вкладка

.
На iPad нажмите Вставка.

Значок

На iPhone коснитесь значка редактирования

Выбор пункта

Советы:

  • Если подходящего формата нет, нажмите Диаграмма на вкладке Вставка, чтобы просмотреть все доступные типы диаграмм.

Вкладка

При выборе макета диаграммы появляется вкладка Диаграмма.

Контекстное меню

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

Маркер

Создание диаграммы на телефоне или планшете с Windows

  1. Откройте книгу и перейдите в электронную таблицу, содержащую данные.
  2. Выберите данные, которые необходимо нанести на диаграмму, перетащив маркеры

Вкладка

.
На планшете с Windows нажмите Вставка.

Дополнительно

Если у вас телефон с Windows, нажмите Дополнительно

Выбор диаграммы на вкладке

Советы:

  • Если подходящего формата нет, нажмите Диаграмма на вкладке Вставка, чтобы просмотреть все доступные типы диаграмм.

Вкладка

При выборе макета диаграммы появляется вкладка Диаграмма.

Контекстное меню

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

Вставка диаграммы в PowerPoint или Word на мобильных устройствах

Примечание: Мы стараемся как можно оперативнее обеспечивать вас актуальными справочными материалами на вашем языке. Эта страница переведена автоматически, поэтому ее текст может содержать неточности и грамматические ошибки. Для нас важно, чтобы эта статья была вам полезна. Просим вас уделить пару секунд и сообщить, помогла ли она вам, с помощью кнопок внизу страницы. Для удобства также приводим ссылку на оригинал (на английском языке) .

В этой версии PowerPoint или Word не удается создать диаграмму. Однако можно создавать диаграммы в Excel и скопируйте его в презентацию или документ.

-

  1. Откройте Excel и выберите книгу, в которой находится на диаграмме.
  2. Коснитесь диаграммы в любом месте, чтобы выбрать ее, а затем нажмите Копировать.

Копирование диаграммы из Excel для iPad

-

Выбор команды «Вставка» для вставки диаграммы в PowerPoint для iPad

  • Перейдите в приложения и перейдите к слайду или документ которой вы хотите вставить диаграмму.
  • Коснитесь на слайде или в документе и нажмите кнопку Вставить.

    Как настроить график в приложении на Android?

    1. Зайдите в раздел «График» в нижней части экрана.

    2. Выберите дни, которые хотите сделать рабочими, после чего на жмите на кнопку карандаша в правом верхнем углу.

    Для компании:

    Image 1504

    Для частного специалиста:

    Image 4081

    3. Настройте время работы и перерыв, если необходимо. После сохраните, нажав на кнопку «Сохранить» в правом верхнем углу.

    CustomView Android. Кольцевая диаграмма для отображения статистики

    Наверное, каждый Android-разработчик на этапе обучения или в процессе выполнения задач в коммерческом проекте задумывался о создании своей CustomView без использования сторонних библиотек, с собственной отрисовкой, анимацией, а также хорошей оптимизацией, чтобы CustomView адаптировалась под переданные ей размеры, правильно отображалась в независимости от экрана пользователя.

    Самый верный и действенный способ постичь Дзен в реализации CustomView является работа над диаграммами и графиками. Чем сложнее будет задуманная диаграмма, тем больше этапов будет пройдено в её реализации: начиная от собственного расчета размеров, заканчивая многоступенчатой анимацией при отрисовке. Каждый из вас способен сделать что-то своё, но порог входа для этого, я соглашусь, достаточно высокий. Поэтому необходим некоторый пример, который поможет разобраться в этом темном лихолесье неизвестности.

    Что ж, в этой статье мы пройдем все этапы реализации своей CustomView, с подробным описанием шагов, чтобы каждый смог повторить такое в своих проектах. Мы будем реализовывать кольцевую диаграмму для отображения какой-либо статистики, добавим много возможностей кастомизации диаграммы под любые виды задач, а также приправим это дело красивой анимацией отрисовки и сохранением состояния. Все будет написано на языке программирования Kotlin.

    Для тех, кто хочет посмотреть на эту красоту сразу, поиграться в Android Studio самостоятельно, исходный код с подробным описанием выложен в публичный доступ на моем GitHub — AnalyticalPieChart

    Постановка задачи

    Самым верным решением при работе над сложным проектом – это постановка цели и определение задач, чтобы понимать, что мы хотим получить в итоговом варианте.

    Наша задача — создание CustomView (AnalyticalPieChart) кольцевой диаграммы, которая должна отображать процентное соотношение значений на круге в виде дуг разного цвета, а также показывать статистику в виде списка пар текстовых значений: название данных — числовое значение.

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

    Список требований к нашей CustomView:

    1. Адаптивность в отрисовке к любым, переданным значениям размеров.
    2. Самостоятельный расчет необходимого размера нашей CustomView.
    3. Возможность изменения ширины круга.
    4. Возможность изменения цветовой палитры.
    5. Возможность добавления расстояния между дугами на круге.
    6. Возможность изменения закругления концов дуг диаграммы.
    7. Возможность изменения размеров круговой диаграммы.
    8. Возможность изменения размеров текста для названия данных и его числового значения.
    9. Возможность изменения цвета текста для названия данных и его числового значения.
    10. Возможность изменения расстояния между текстом.
    11. Возможность изменения радиуса круга рядом с числовым значением данных.
    12. Собственная анимация.
    13. Собственная реализация сохранения состояния CustomView при configuration changes.

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

    Подготовительный этап

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

    Extensions

    Во всех расчетах View мы будем использовать значение единицы px. Для правильного отображения есть необходимость в переводе dp и sp в px. Поэтому добавляем следующие extension функции для Context.

    /** * Context Extension для конвертирования значения в пиксели. * @property dp - значение density-independent pixels */ fun Context.dpToPx(dp: Int): Float < return dp.toFloat() * this.resources.displayMetrics.density >/** * Context Extension для конвертирования значения размера шрифта в пиксели. * @property sp - значение scale-independent pixels */ fun Context.spToPx(sp: Int): Float

    Далее добавим extension функцию хелпер в класс StaticLayout для отрисовки текста на нашей View.

    /** * StaticLayout Extension для удобства отрисовки текста. */ fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) < canvas.withTranslation(x, y) < draw(this) >>
    Model

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

    Создадим модель AnalyticalPieChartModel

    /** * Представляет собой модель хранения смежной информации о рисуемом объекте диаграммы. * * После добавления переменных в primary constructor, вызывается блок init <>, * в котором переданные в модель значения ампроксимируются к значениям процента круговой диаграммы. * * Модель состоит и следующих параметров: * @property percentOfCircle - значение занимаемого процента круговой диаграммы. * @property percentToStartAt - значение положения на круговой диаграмме, * откуда должен начать отрисовываться объект. * @property colorOfLine - значение цвета для отрисовки линии объекта. * @property stroke - значение ширины линии объекта. * @property paint - объект кисти отрисовки * @property paintRound - закругление концов линии объекта. */ data class AnalyticalPieChartModel( var percentOfCircle: Float = 0F, var percentToStartAt: Float = 0F, var colorOfLine: Int = 0, var stroke: Float = 0F, var paint: Paint = Paint(), var paintRound: Boolean = true ) < /** * Блок, в котором значения преобразуются к приближенным значениям круговой диаграммы. * То есть в модель передается процент (от 0 до 100). */ init < // Проверка на корректность переданного процента. if (percentOfCircle < 0 || percentOfCircle >100) < percentOfCircle = 100F >// Расчет переданного значения на круговой диаграмме. percentOfCircle = 360 * percentOfCircle / 100 // Проверка на корректность переданного процента. if (percentToStartAt < 0 || percentToStartAt >100) < percentToStartAt = 0F >// Расчет переданного значения на круговой диаграмме. percentToStartAt = 360 * percentToStartAt / 100 /** Установка своего цвета в случаи пропуска [colorOfLine] */ if (colorOfLine == 0) < colorOfLine = Color.parseColor("#000000") >// Инициализация кисти для отрисовки paint = Paint() paint.color = colorOfLine paint.isAntiAlias = true paint.style = Paint.Style.STROKE paint.strokeWidth = stroke paint.isDither = true; // Проверка необходимости закругления концов линии объекта. if (paintRound) < paint.strokeJoin = Paint.Join.ROUND; paint.strokeCap = Paint.Cap.ROUND; paint.pathEffect = CornerPathEffect(8F); >> >
    SavedState

    Кроме этого, мы в требованиях к нашей CustomView указали, что наша View должна самостоятельно сохранять свое состояние. Поэтому нам необходим свой класс, который будет наследоваться от BaseSavedState и имплементировать интерфейс Parcelable. Этот класс как раз-таки будет отвечать за сохранение и получение наших данных в onSaveInstanceState() и методе жизненного цикла View — onRestoreInstanceState().

    Создадим класс AnalyticalPieChartState

    /** * Собственный state для сохранения и восстановления данных */ class AnalyticalPieChartState( private val superSavedState: Parcelable?, val dataList: List> ) : BaseSavedState(superSavedState), Parcelable
    Добавление AnalyticalPieChart

    Сперва необходимо создать интерфейс взаимодействия со View. Добавим интерфейс AnalyticalPieChartInterface, в котором будут написаны основные функции взаимодействия: setDataChart – для добавления данных, startAnimation – для старта анимации.

    /** * Интерфейс для взаимодействия с CustomView AnalyticalPieChart */ interface AnalyticalPieChartInterface < /** * Метод для добавления списка данных для отображения на графике. * @property list - список данных, тип которого мы можете поменять * на свою определенную модель. */ fun setDataChart(list: List>) /** * Метод для активирования анимации прорисовки. */ fun startAnimation() >

    Создадим нашу CustomView, которую будем в дальнейшем дополнять кодом, и имплементируем ранее созданный интерфейс AnalyticalPieChartInterface.

    /** * CustomView AnalyticalPieChart * * Кольцевая диаграмма для отображения статистики объектов в процентном соотношении. */ class AnalyticalPieChart @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr), AnalyticalPieChartInterface < /** * Имплиментируемый метод интерфейса взаимодействия [AnalyticalPieChartInterface]. * Добавление данных в View. */ override fun setDataChart(list: List>) <> /** * Имплиментируемый метод интерфейса взаимодействия [AnalyticalPieChartInterface]. * Запуск анимации отрисовки View. */ override fun startAnimation() <> >

    Вниманию тут нужно уделить лишь аннотации @JvmOverloads. Она информирует компилятор, что следует создать конструктор на основе предыдущего с дополнительным параметром со значением по умолчанию. Этим мы избегаем бойлерплейт кода.

    Файлы ресурсов

    Добавим массив цветов для отображения данных.

     Analytical Pie Chart Всего #E480F4 #6CC3F3 #7167ED #D9455F #6054EA  

    Внутри конструкции задекларируем наши кастомные Attrs, которые позволят нам взаимодействовать с полями класса AnalyticalPieChart сразу в xml разметке. О том, что значит каждый из атрибутов, мы рассмотрим дальше, когда будем добавлять поля в CustomView.

    Добавим нашу View в xml разметку Activity.

    Напоминаю, кто хочет разобраться в проекте самостоятельно, всегда прошу, исходники в репозитории GitHub — AnalyticalPieChart

    Реализация

    Добавление полей в AnalyticalPieChart. Init блок.

    CustomView по требованиям должна быть максимально адапативной и кастомизируемой под любой тип значит. Это означает, что нам необходимо достаточно много полей, которые бы отвечали за то или иное изменение при отображении. Добавим поля в наш класс AnalyticalPieChart.

    /** * CustomView AnalyticalPieChart * * Кольцевая диаграмма для отображения статистики объектов в процентном соотношении. * * AnalyticalPieChart адаптируется под любое значение высоты и ширины, которое передается * данной View от parent. * Проверено на всех возможных разрешениях экрана (ldpi, mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi). * * Для удобства использования и сборки в DI, класс имплементирует интерфейс взаимодействия [AnalyticalPieChartInterface] * * * AnalyticalPieChart обладает огромным количество настроек отображения. * Все расчеты производятся в пикселях, необходимо это учитывать при изменении кода. * * @property marginTextFirst - значение отступа между числом и его описанием. * @property marginTextSecond - значение отступа между объектами, где объект - число и описание. * @property marginTextThird - значение отступа между числом и его описанием общего результата. * @property marginSmallCircle - значение отступа между числом и маленьким кругом. * @property marginText - значение суммы отступов [marginTextFirst] и [marginTextSecond]. * @property circleRect - объект отрисовки круговой диаграммы. * @property circleStrokeWidth - значение толщины круговой диаграммы. * @property circleRadius - значение радиуса круговой диаграммы. * @property circlePadding - padding для всех сторон круговой диаграммы. * @property circlePaintRoundSize - значение округления концов линий объектов круга. * @property circleSectionSpace - значение расстояние-процент между линиями круга. * @property circleCenterX - значение координаты X центра круговой диаграммы. * @property circleCenterY - значение координаты Y центра круговой диаграммы. * @property numberTextPaint - объект кисти отрисовки текста чисел. * @property descriptionTextPain - объект кисти отрисовки текста описания. * @property amountTextPaint - объект кисти отрисовки текста результата. * @property textStartX - значение координаты X, откуда отрисовывается текст. * @property textStartY - значение координаты Y, откуда отрисовывается текст. * @property textHeight - значение высоты текста. * @property textCircleRadius - значение радиуса малого круга около текста числа. * @property textAmountStr - строка результата. * @property textAmountY - значение координаты Y, откуда отрисовывается результирующий текст. * @property textAmountXNumber - значение координаты X, откуда отрисовывается результирующий текст числа. * @property textAmountXDescription - значение координаты X, откуда отрисовывается описание результата. * @property textAmountYDescription - значение координаты Y, откуда отрисовывается описание результата. * @property totalAmount - итоговый результат - сумма значений Int в [dataList]. * @property pieChartColors - список цветов круговой диаграммы в виде текстового представления. * @property percentageCircleList - список моделей для отрисовки. * @property textRowList - список строк, которые необходимо отобразить. * @property dataList - исходный список данных. * @property animationSweepAngle - переменная для анимации. */ class AnalyticalPieChart @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr), AnalyticalPieChartInterface < /** * Базовые значения для полей и самой [AnalyticalPieChart] */ companion object < private const val DEFAULT_MARGIN_TEXT_1 = 2 private const val DEFAULT_MARGIN_TEXT_2 = 10 private const val DEFAULT_MARGIN_TEXT_3 = 2 private const val DEFAULT_MARGIN_SMALL_CIRCLE = 12 private const val ANALYTICAL_PIE_CHART_KEY = "AnalyticalPieChartArrayData" /* Процент ширины для отображения текста от общей ширины View */ private const val TEXT_WIDTH_PERCENT = 0.40 /* Процент ширины для отображения круговой диаграммы от общей ширины View */ private const val CIRCLE_WIDTH_PERCENT = 0.50 /* Базовые значения ширины и высоты View */ const val DEFAULT_VIEW_SIZE_HEIGHT = 150 const val DEFAULT_VIEW_SIZE_WIDTH = 250 >private var marginTextFirst: Float = context.dpToPx(DEFAULT_MARGIN_TEXT_1) private var marginTextSecond: Float = context.dpToPx(DEFAULT_MARGIN_TEXT_2) private var marginTextThird: Float = context.dpToPx(DEFAULT_MARGIN_TEXT_3) private var marginSmallCircle: Float = context.dpToPx(DEFAULT_MARGIN_SMALL_CIRCLE) private val marginText: Float = marginTextFirst + marginTextSecond private val circleRect = RectF() private var circleStrokeWidth: Float = context.dpToPx(6) private var circleRadius: Float = 0F private var circlePadding: Float = context.dpToPx(8) private var circlePaintRoundSize: Boolean = true private var circleSectionSpace: Float = 3F private var circleCenterX: Float = 0F private var circleCenterY: Float = 0F private var numberTextPaint: TextPaint = TextPaint() private var descriptionTextPain: TextPaint = TextPaint() private var amountTextPaint: TextPaint = TextPaint() private var textStartX: Float = 0F private var textStartY: Float = 0F private var textHeight: Int = 0 private var textCircleRadius: Float = context.dpToPx(4) private var textAmountStr: String = "" private var textAmountY: Float = 0F private var textAmountXNumber: Float = 0F private var textAmountXDescription: Float = 0F private var textAmountYDescription: Float = 0F private var totalAmount: Int = 0 private var pieChartColors: List = listOf() private var percentageCircleList: List = listOf() private var textRowList: MutableList = mutableListOf() private var dataList: List> = listOf() private var animationSweepAngle: Int = 0 /** * В INIT блоке инициализируются все необходимые поля и переменные. * Необходимые значения вытаскиваются из специальных Attr тегов * (). */ init < // Задаем базовые значения и конвертируем в px var textAmountSize: Float = context.spToPx(22) var textNumberSize: Float = context.spToPx(20) var textDescriptionSize: Float = context.spToPx(14) var textAmountColor: Int = Color.WHITE var textNumberColor: Int = Color.WHITE var textDescriptionColor: Int = Color.GRAY // Инициализируем поля View, если Attr присутствуют if (attrs != null) < val typeArray = context.obtainStyledAttributes(attrs, R.styleable.AnalyticalPieChart) // Секция списка цветов val colorResId = typeArray.getResourceId(R.styleable.AnalyticalPieChart_pieChartColors, 0) pieChartColors = typeArray.resources.getStringArray(colorResId).toList() // Секция отступов marginTextFirst = typeArray.getDimension(R.styleable.AnalyticalPieChart_pieChartMarginTextFirst, marginTextFirst) marginTextSecond = typeArray.getDimension(R.styleable.AnalyticalPieChart_pieChartMarginTextSecond, marginTextSecond) marginTextThird = typeArray.getDimension(R.styleable.AnalyticalPieChart_pieChartMarginTextThird, marginTextThird) marginSmallCircle = typeArray.getDimension(R.styleable.AnalyticalPieChart_pieChartMarginSmallCircle, marginSmallCircle) // Секция круговой диаграммы circleStrokeWidth = typeArray.getDimension(R.styleable.AnalyticalPieChart_pieChartCircleStrokeWidth, circleStrokeWidth) circlePadding = typeArray.getDimension(R.styleable.AnalyticalPieChart_pieChartCirclePadding, circlePadding) circlePaintRoundSize = typeArray.getBoolean(R.styleable.AnalyticalPieChart_pieChartCirclePaintRoundSize, circlePaintRoundSize) circleSectionSpace = typeArray.getFloat(R.styleable.AnalyticalPieChart_pieChartCircleSectionSpace, circleSectionSpace) // Секция текста textCircleRadius = typeArray.getDimension(R.styleable.AnalyticalPieChart_pieChartTextCircleRadius, textCircleRadius) textAmountSize = typeArray.getDimension(R.styleable.AnalyticalPieChart_pieChartTextAmountSize, textAmountSize) textNumberSize = typeArray.getDimension(R.styleable.AnalyticalPieChart_pieChartTextNumberSize, textNumberSize) textDescriptionSize = typeArray.getDimension(R.styleable.AnalyticalPieChart_pieChartTextDescriptionSize, textDescriptionSize) textAmountColor = typeArray.getColor(R.styleable.AnalyticalPieChart_pieChartTextAmountColor, textAmountColor) textNumberColor = typeArray.getColor(R.styleable.AnalyticalPieChart_pieChartTextNumberColor, textNumberColor) textDescriptionColor = typeArray.getColor(R.styleable.AnalyticalPieChart_pieChartTextDescriptionColor, textDescriptionColor) textAmountStr = typeArray.getString(R.styleable.AnalyticalPieChart_pieChartTextAmount) ?: "" typeArray.recycle() >circlePadding += circleStrokeWidth // Инициализация кистей View initPains(amountTextPaint, textAmountSize, textAmountColor) initPains(numberTextPaint, textNumberSize, textNumberColor) initPains(descriptionTextPain, textDescriptionSize, textDescriptionColor, true) > /** * Имплиментируемый метод интерфейса взаимодействия [AnalyticalPieChartInterface]. * Добавление данных в View. */ override fun setDataChart(list: List>) < dataList = list calculatePercentageOfData() >/** * Имплиментируемый метод интерфейса взаимодействия [AnalyticalPieChartInterface]. * Запуск анимации отрисовки View. */ override fun startAnimation() <> /** * Метод инициализации переданной TextPaint */ private fun initPains(textPaint: TextPaint, textSize: Float, textColor: Int, isDescription: Boolean = false) < textPaint.color = textColor textPaint.textSize = textSize textPaint.isAntiAlias = true if (!isDescription) textPaint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) >>

    Комментарий к каждому полю вынесен перед классом для того, чтобы было удобнее копировать. Также стоит пояснить, что в начале Init блока значения размеров и цвета текста заданы с дефолтными значениями. Это сделано для того, чтобы объекты TextPaint были точно проинициализированы. В случае если attr будут отсутствовать — никакой текст не отобразиться на экране.

    В блоке If мы вытаскиваем значения переданные View из xml разметки, не забывая при этом очищать объект TypeArray. После этого блока стоит обратить внимание на то, что к circlePadding мы прибавляем circleStrokeWidth (толщина круга). Это сделано для того, чтобы учитывать в отступах сразу толщину круга. Если бы этого не было, то круговая диаграмма, при нулевом circlePadding, выходила бы за края.

    Специально для вас я подготовил шаблон CustomView, в котором отображается: какое полей и за что отвечает. На этой схеме указаны не все поля, которые добавлены в нашу View, так как некоторые поля нужны лишь для сохранения расчетов и отображения. Кроме того, изменения цвета и ширины текста тоже не указаны на данном шаблоне, поскольку они достаются в Init блоке и записываются сразу в объекты TextPaint.

    OnMeasure()

    Метод OnMeasure() — это метод, в котором View задает свои размеры, являясь важной частью контракта между view и layout. onMeasure() вызывается после вызова метода measure(). Данный метод вызывается layout-ом для всех дочерних view, чтобы определить их размеры.

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

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

    Расчет размеров

    Перед тем, как реализовывать расчет размеров нашей View в методе onMeasure(), нам необходимы дополнительные методы, которые позволят нам совместить наши расчеты вместе и не дублировать код. Будем собирать метод onMeasure() по кусочкам.

    Сперва добавим метод resolveDefaultSize. Данный метод служит для поверки режима, с которым передается значение, в котором закодирована информация о предпочтениях layout к размерам View, и установки базового значения View в случаи, если у parent layout нет предпочтений к размерам нашей CustomView. В остальных случаях мы беспрекословно слушаемся наш parent layout, и возвращаем размер, который от нас требуется.

    /** * Метод получения размера View по переданному Mode. */ private fun resolveDefaultSize(spec: Int, defValue: Int): Int < return when(MeasureSpec.getMode(spec)) < MeasureSpec.UNSPECIFIED ->context.dpToPx(defValue).toInt() // Размер не определен parent layout else -> MeasureSpec.getSize(spec) // Слушаемся parent layout > >

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

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

    Добавим метод getMultilineText, который будет нам создавать и возвращать объекты класса StaticLayout с заданными параметрами. Более подробно о возможностях данного класса вы сможете найти в официальной документации.

    /** * Метод создания [StaticLayout] с переданными значениями */ private fun getMultilineText( text: CharSequence, textPaint: TextPaint, width: Int, start: Int = 0, end: Int = text.length, alignment: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL, textDir: TextDirectionHeuristic = TextDirectionHeuristics.LTR, spacingMult: Float = 1f, spacingAdd: Float = 0f) : StaticLayout

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

    Кроме того, текст необходимо и отрисовать. Поэтому объекты StaticLayout мы будем сохранять в список textRowList, который будет использоваться при отрисовки. Если же мы созданные объекты не сохраняли бы в список, то при отрисовке нам нужно было бы опять их создавать, а при отображении нам ни в коем случае нельзя создавать какие-либо объекты. Ответ на вопрос «Почему?» будет дальше в секции про отрисовку.

    /** * Метод расчёта высоты объектов, где объект - это число и его описание. * Добавление объекта в список строк для отрисовки [textRowList]. */ private fun getTextViewHeight(maxWidth: Int): Int < var textHeight = 0 // Проходимся по всем данным, которые передали в View dataList.forEach < // Создаем объект StaticLayout для значения данных val textLayoutNumber = getMultilineText( text = it.first.toString(), textPaint = numberTextPaint, width = maxWidth ) // Создаем объект StaticLayout для описания значения данных val textLayoutDescription = getMultilineText( text = it.second, textPaint = descriptionTextPain, width = maxWidth ) // Сохраняем объекты в список для отрисовки textRowList.apply < add(textLayoutNumber) add(textLayoutDescription) >// Складываем высоты текстов textHeight += textLayoutNumber.height + textLayoutDescription.height > return textHeight >

    Теперь мы можем рассчитать высоту AnalyticalPieChart с помощью метода calculateViewHeight. Сначала мы получаем размеры высота View из созданного ранее метода resolveDefaultSize. Далее находим высоту всего текста, учитывая отступы между разными строками. И уже высчитываем высоту самого View, учитывая padding.

    В конце мы делаем проверку, если рассчитанная нами высота View с padding и margin больше, чем высота, которую нам дает наш parent, то мы будем возвращать именно нами рассчитанную высоту. Данная проверка делается для того, чтобы избежать случаев, когда у нас немного текста, и он сможет поместиться во View без дополнительного размера.

    /** * Метод расчёта высоты всего текста, включая отступы. */ private fun calculateViewHeight(heightMeasureSpec: Int, textWidth: Int): Int < // Получаем высоту, которую нам предлагает parent layout val initSizeHeight = resolveDefaultSize(heightMeasureSpec, DEFAULT_VIEW_SIZE_HEIGHT) // Высчитываем высоту текста с учетом отступов textHeight = (dataList.size * marginText + getTextViewHeight(textWidth)).toInt() // Добавляем к значению высоты вертикальные padding View val textHeightWithPadding = textHeight + paddingTop + paddingBottom return if (textHeightWithPadding >initSizeHeight) textHeightWithPadding else initSizeHeight >

    Заключительным этапом будет как раз-таки переопределение метода onMeasure. Добавим его в AnalyticalPieChart.

    /** * Метод жизненного цикла View. * Расчет необходимой ширины и высоты View. */ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) < // Очищаем список строк для текста textRowList.clear() // Получаем ширину View val initSizeWidth = resolveDefaultSize(widthMeasureSpec, DEFAULT_VIEW_SIZE_WIDTH) // Высчитываем ширину, которую будет занимать текст val textTextWidth = (initSizeWidth * TEXT_WIDTH_PERCENT) // Вычисляем необходимую высоту для нашей View val initSizeHeight = calculateViewHeight(heightMeasureSpec, textTextWidth.toInt()) // Координаты X и Y откуда будет происходить отрисовка текста textStartX = initSizeWidth - textTextWidth.toFloat() textStartY = initSizeHeight.toFloat() / 2 - textHeight / 2 calculateCircleRadius(initSizeWidth, initSizeHeight) setMeasuredDimension(initSizeWidth, initSizeHeight) >/** * Метод расчёта радиуса круговой диаграммы, установка координат для отрисовки. */ private fun calculateCircleRadius(width: Int, height: Int) < // Рассчитываем ширину, которую будет занимать диаграмма val circleViewWidth = (width * CIRCLE_WIDTH_PERCENT) // Высчитываем радиус круга диаграммы circleRadius = if (circleViewWidth >height) < (height.toFloat() - circlePadding) / 2 >else < circleViewWidth.toFloat() / 2 >// Установка расположения круговой диаграммы на View with(circleRect) < left = circlePadding top = height / 2 - circleRadius right = circleRadius * 2 + circlePadding bottom = height / 2 + circleRadius >// Координаты центра круговой диаграммы circleCenterX = (circleRadius * 2 + circlePadding + circlePadding) / 2 circleCenterY = (height / 2 + circleRadius + (height / 2 - circleRadius)) / 2 textAmountY = circleCenterY // Создаем контейнер для отображения текста в центре круговой диаграммы val sizeTextAmountNumber = getWidthOfAmountText( totalAmount.toString(), amountTextPaint ) // Расчет координат для отображения текста в центре круговой диаграммы textAmountXNumber = circleCenterX - sizeTextAmountNumber.width() / 2 textAmountXDescription = circleCenterX - getWidthOfAmountText(textAmountStr, descriptionTextPain).width() / 2 textAmountYDescription = circleCenterY + sizeTextAmountNumber.height() + marginTextThird > /** * Метод обертки текста в класс [Rect] */ private fun getWidthOfAmountText(text: String, textPaint: TextPaint): Rect

    Сначала мы отчищаем список строк для отображения. Получаем ширину View через метод resolveDefaultSize, берем процент от этой ширины под наш текст и рассчитываем высоту нашей View. Здесь же находим X и Y координаты откуда будет выводиться текст. И обязательно вызываем setMeasuredDimension, в который передаем размеры нашей View. Если не вызывать этот метод, будет вызвана ошибка IllegalStateException.

    Метод calculateCircleRadius() — рассчитывает размеры круговой диаграммы, учитывая размеры AnalyticalPieChart и отступы, координаты для вывода результата в центре диаграммы, а также сразу устанавливает расположение диаграммы на View.

    OnDraw() — отрисовка AnalyticalPieChart

    Как я говорил ранее, при отрисовке (метод onDraw()) нам необходимо избегать создание объектов и сложных вычислений. Так как создание новых объектов может спровоцировать сборку мусора, что приводит к паузам и лагам пользовательского UI. Именно поэтому все необходимые вычисления и инициализации объектов мы выполняли до отрисовки самой View.

    Безотлагательно переходим к отображению нашей View на экране пользователя. Нам необходимо отображать две различные области: диаграмма, данные. Поэтому мы вынесем отрисовку этих областей в разные методы, чтобы код был более читабельным и удобным для изменений.

    Отрисовка диаграммы

    Отображение диаграммы тесно связана с анимацией, поэтому добавим сначала ValueAnimator, который будет проходить значения от 0 до 360 (полный оборот круга). Для более идеальной анимации воспользуемся интерпретатором FastOutSlowInInterpolator, который ускоряет анимацию в начале и замедляет в конце. Значения, которые будет проходить ValueAnimator, мы будем запоминать в переменной animationSweepAngle.

    Важно отметить, что в нашем ValueAnimator кроме запоминания значения, мы также должны вызвать метод invalidate(). Это метод, который инициирует принудительную перерисовку определенного представления. Проще говоря, метод invalidate() следует вызывать в случае, когда требуется изменение внешнего вида представления. У нас изменилось значение — необходима перерисовка.

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

    Метод startAnimation будет выглядеть следующим образом.

    /** * Имплиментируемый метод интерфейса взаимодействия [AnalyticalPieChartInterface]. * Запуск анимации отрисовки View. */ override fun startAnimation() < // Проход значений от 0 до 360 (целый круг), с длительностью - 1.5 секунды val animator = ValueAnimator.ofInt(0, 360).apply < duration = 1500 // длительность анимации в миллисекундах interpolator = FastOutSlowInInterpolator() // интерпретатор анимации addUpdateListener < valueAnimator ->// Обновляем значение для отрисовки диаграммы animationSweepAngle = valueAnimator.animatedValue as Int // Принудительная перерисовка invalidate() > > animator.start() >

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

    Добавим в AnalyticalPieChart метод drawCircle(canvas: Canvas), в который мы передаем Canvas для отображения. Метод будет выглядеть следующим образом.

    /** * Метод отрисовки круговой диаграммы на Canvas. */ private fun drawCircle(canvas: Canvas) < // Проходимся по дугам круга for(percent in percentageCircleList) < // Если процент дуги попадает под угол отрисовки (animationSweepAngle) // Отображаем эту дугу на Canvas if (animationSweepAngle >percent.percentToStartAt + percent.percentOfCircle) < canvas.drawArc(circleRect, percent.percentToStartAt, percent.percentOfCircle, false, percent.paint) >else if (animationSweepAngle > percent.percentToStartAt) < canvas.drawArc(circleRect, percent.percentToStartAt, animationSweepAngle - percent.percentToStartAt, false, percent.paint) >> >
    Отрисовка данных

    Отображение же данных более сложная задача, чем отображение круговой диаграммы. Сперва надо понять, что мы уже создали объекты текста и записали в список textRowList. Но по нашей задумке, нам необходимо отобразить со значением данных маленький круг с цветом этих данных. Кроме того, данные у нас разделяются таким образом, что каждый второй элемент в списке textRowList является как раз-таки значением данных. Нам необходимо отслеживать на какой высоте отображать наш текст и круг, поэтому мы будем увеличивать textBuffY (хранит координату Y для отображения текста).

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

    Добавим метод drawText(canvas: Canvas), и он будет выглядеть следующим образом.

    /** * Метод отрисовки всего текста диаграммы на Canvas. */ private fun drawText(canvas: Canvas) < // Отслеживаем Y координату при отображении текста var textBuffY = textStartY // Проходимся по каждой строке textRowList.forEachIndexed < index, staticLayout ->// Если это у нас значение данных, то отображаем заполненный круг и текст значения if (index % 2 == 0) < staticLayout.draw(canvas, textStartX + marginSmallCircle + textCircleRadius, textBuffY) canvas.drawCircle( textStartX + marginSmallCircle / 2, textBuffY + staticLayout.height / 2 + textCircleRadius / 2, textCircleRadius, Paint().apply < color = Color.parseColor(pieChartColors[(index / 2) % pieChartColors.size]) >) // Прибавляем высоту и отступ к координате Y textBuffY += staticLayout.height + marginTextFirst > else < // Отображаем описание значения staticLayout.draw(canvas, textStartX, textBuffY) textBuffY += staticLayout.height + marginTextSecond >> // Отображаем текстовый результат в центре круговой диаграммы canvas.drawText(totalAmount.toString(), textAmountXNumber, textAmountY, amountTextPaint) canvas.drawText(textAmountStr, textAmountXDescription, textAmountYDescription, descriptionTextPain) >

    Мы уже движемся к финалу отображения нашей View на экране. Нам необходимо просто вызвать наши созданные ранее методы в onDraw(). Поэтому метод onDraw() будет выглядеть следующим образом.

    /** * Метод жизненного цикла View. * Отрисовка всех необходимых компонентов на Canvas. */ override fun onDraw(canvas: Canvas)

    Чтобы увидеть результат, нам остается лишь в методе onStart() нашей Activity отправить данные в AnalyticalPieChart и вызвать старт анимации. Я делаю вызовы для примера, где и как вызывать их решайте сами — как вам будет удобно.

    class MainActivity : AppCompatActivity() < private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) < super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) >override fun onStart() < super.onStart() binding.analyticalPieChart1.setDataChart( listOf(Pair(4, "Свои проекты"), Pair(6, "Соместные проекты"), Pair(6, "Проекты поддержанные группой людей"), Pair(2, "Неизвестные проекты")) ) >>

    Запускаем наше приложение и получаем красивый результат.

    Сохранение состояния

    Осталось совсем немного для завершения нашей CustomView. Нам необходимо сохранять состояние нашей View при тех случаях, когда происходит configuration changes (поворот экрана, смена языковой темы и т. д.).

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

    Переопределим методы onSaveInstanceState() и onRestoreInstanceState() следующим образом.

    /** * Восстановление данных из [AnalyticalPieChartState] */ override fun onRestoreInstanceState(state: Parcelable?) < val analyticalPieChartState = state as? AnalyticalPieChartState super.onRestoreInstanceState(analyticalPieChartState?.superState ?: state) dataList = analyticalPieChartState?.dataList ?: listOf() >/** * Сохранение [dataList] в собственный [AnalyticalPieChartState] */ override fun onSaveInstanceState(): Parcelable

    Если же состояние вдруг не пришло или отсутствует, то в dataList мы записываем пустой список.

    Заключение

    Что ж, я очень надеюсь, что данная статья дала кому-то понимание того, как можно создавать свои CustomView. Всегда необходимо начинать с постановки задачи, если будет хорошо расписан результат, то и разработка будет вестись проще и быстрее.

    В итоге мы получили CustomView — AnalyticalPieChart, которая соответствует всем требованиям, которые мы выдвинули с самого начала. Она адаптируется самостоятельно под любой размер, отображает и диаграмму, и данные; позволяет изменить очень много параметров, которые влияют на итоговый результат; дает возможность использовать её в разных типах задач.

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

    • android
    • custom view
    • customization
    • android development
    • разработка
    • кастом
    • android studio
    • android sdk
    • с нуля
    • нестандартные решения
    • Разработка мобильных приложений
    • Разработка под Android
    • Kotlin
    • Дизайн мобильных приложений
  • Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *