суббота, 18 сентября 2010 г.

Вертикальный ProgressBar в Android

Возникла недавно необходимость в оном. Стандартного не оказалось, пришлось писать свой.


Собственные контролы в андроиде бывают нескольких видов:
  1. Расширение существующего контрола. Например, при написании собственного ToggleButton имеет смысл наследоваться от Button.
  2. Составные компоненты. Например, контрол NumberPicker можно представить, как комбинацию двух кнопок (+ и ) и TextView, в котором выводится текущее значение. При разработке подобных контролов можно наследоваться от всяческих Layout-ов, а расположение контролов задавать либо в коде, либо в XML-разметке.
  3. Полностью настраиваемые компоненты. Когда ни один из существующих контролов не обладает нужной фунцкциональностью, мы реализуем всю логику и отрисовку компонента сами.
Как несложно догадаться, наш вариант последний. При разработке полностью настраиваимых компонентов правила простые:
  • Наследуемся от View.
  • Реализуем метод onMeasure, в котором выделяем место под компонент.
  • Реализуем метод onDraw, осуществляющий отрисовку компонента.
  • Ну и конечно, по ходу пишем логику работы компонента, добавляем листенеры и т.д.
А теперь, иллюстрируя сказанное, представлю код класса VerticalProgressBar

VerticalProgressBar.java

public class VerticalProgressBar extends View 
{
    private static final int WIDTH = 20;
    
    private static final int REMAIN = Color.rgb(49, 49, 49);
    private static final int PROCEED = Color.rgb(22, 72, 237);
    private static final int FINISHED = Color.rgb(124, 209, 15);
    
    private int mHeight;
    private int mWidth;
    
    private int mProgress;
    private int mMax;

    public VerticalProgressBar(Context context)
    {
        this(context, null);
    }

    public VerticalProgressBar(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }
    
    public synchronized void incrementProgressBy(int delta)
    {
        this.setProgress(mProgress + delta);
    }

    public synchronized void setProgress(int progress)
    {
        if (progress < 0)
        {
            progress = 0;
        }

        if (progress > mMax)
        {
            progress = mMax;
        }

        if (progress != mProgress)
        {
            mProgress = progress;
            refreshProgress();
        }
    }

    public synchronized void setMax(int max)
    {
        this.mMax = max;
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        super.onDraw(canvas);
        
        Paint paint = new Paint();
        paint.setColor(REMAIN);
        paint.setStyle(Style.FILL);

        if (mProgress == 0)
        {
            canvas.drawRect(0, 0, this.mWidth, this.mHeight, paint);
        }
        else if (mProgress >= mMax)
        {
            paint.setColor(FINISHED);
            paint.setStyle(Style.FILL);
            canvas.drawRect(0, 0, this.mWidth, this.mHeight, paint);
        }
        else
        {
            float proceedHeight = ((float) mProgress / mMax) * (float) this.mHeight;

            canvas.drawRect(0, proceedHeight, this.mWidth, this.mHeight, paint);
            paint.setColor(PROCEED);
            paint.setStyle(Style.FILL);
            canvas.drawRect(0, 0, this.mWidth, proceedHeight, paint);
        }
        paint.setColor(Color.BLACK);
        paint.setStyle(Style.FILL);
        canvas.drawLine(0, this.mHeight, this.mWidth, this.mHeight, paint);
        canvas.drawLine(0, 0, this.mWidth, 0, paint);

    }
    
    @Override
    protected void onMeasure(int widthSpecId, int heightSpecId)
    {
        this.mHeight = View.MeasureSpec.getSize(heightSpecId);
        this.mWidth = WIDTH; 
        
        setMeasuredDimension(this.mWidth, this.mHeight);
    }
    
    private synchronized void refreshProgress()
    {
        invalidate();
    }
}
Я уже не стану добавлять тут никаких событий и листенеров, это не входит в тему статьи. Желающие могут дорабатывать этот класс самостоятельно.
А вот и пример работы:
VerticalProgressBar

А можно немного поменять способ отрисовки, и получится вот что:
VerticalDottedProgressBar
Код этого класса я представлять не буду, желающие могут посмотреть исходники примера.

12 комментариев:

jeck_landin комментирует...

прикольно.
возможно есть способ проще - перегрузить ProgressBar.onDraw примерно так:

canvas.setMatrix(rotate270matrix);
super(canvas)

darja комментирует...

Оригинально, мне такое даже в голову не пришло.
Правда, моей задачей была именно "зебра", а ProgressBar так вроде не умеет.

Анонимный комментирует...

Спасибо за очередную отличную статью!
А не подскажете ли, никак не соображу:
- есть TextView
- нужно, чтобы в нём отображалось текущее время (например, 14:20) (системное как бы)

Как это реализовать?

jeck_landin комментирует...

На самом деле умеет, для этого нужно ему скармливать специальные xml'ки (например с ClipDrawable). Другое дело что документации по этому я не нашел, но в исходниках это видно.

Мур Вотема комментирует...

jeck_landin и darja
Уже несколько раз читал, что видно в исходниках. А где эти исходники лежат?!

Документация часто написана только для простых случаев. Как только становится сложнее, документации на это не найти.

darja комментирует...

2 Мур Вотема:
http://android.git.kernel.org/

Там много всякого интересного. А контролы лежат в platform/frameworks/base.git и дальше в /core/java/android/ и т.д.

Unknown комментирует...

Познавательно! Спасибо!
Будет ли обзор создания собственного листнера событий?)
Создал свой в отдельном потоке - сильно грузит процессор.

Mr. Wertugo комментирует...

Соглашусь с jeck_landin. Делал вертикальный прогресс. Переопределял стандартный progressBar, рисование. Если нужно переопределение жестов изменения прогресса тоже не сложная задача.

Villy комментирует...
Этот комментарий был удален автором.
Villy комментирует...

Здравствуйте, Дарья.

У меня в проге будет пара десятков кнопочек, немного отличающихся друг от друга.

Я затеял MyButton -> Button, прописав все базовые свойства в конструкторах, вот так


...
Resources _res = getResources();

setBackgroundResource(R.drawable.button_selector);
setHeight((int)_res.getDimension(R.dimen.button_height));

ColorStateList _csl = _res.getColorStateList(R.drawable.color_selector);

setTextColor(_csl);

,

предполагая, что впоследствии в XML смогу кое-какие изменять, типа


com.example.picafama.MyButton

android:id="@+id/button7"

android:height="80dp"

android:text="@string/digit7"

android:textColor="@android:color/black"



Однако, свойства не меняются... В частности, android:height и android:textColor остаются такими, какие назначены у класса.

Подскажите, плз. реализуема ли такая вроде рядовая техника кастомизации объектов класса? Где и в чем тонкости?

PS. В XML-сниппете я убрал теги у com.example.picafama.MyButton, а то парсер блога резал весь код...

С уважением. Валерий

darja комментирует...

Я так понимаю, конструктор выглядит как-то так:

public MyButton(Context context, AttributeSet attrs)
{
super(context, attrs);
...
Resources _res = getResources();

setBackgroundResource(R.drawable.button_selector);
setHeight((int)_res.getDimension(R.dimen.button_height));

ColorStateList _csl = _res.getColorStateList(R.drawable.color_selector);

setTextColor(_csl);
}

Тогда всё правильно. При вызове конструктора базового класса выставились значения атрибутов из XML, а потом перезаписались захардкоженными значениями (setHeight, setTextColor и пр.). Если нужны значения из XML — уберите эти вызовы. Если нужны захардкоженные значения — всё и так работает. Если нужно, чтобы захардкоженные значения были значениями по умолчанию — придётся городить огород с атрибутами, как рассказано здесь

Villy комментирует...

угу... порыл исходники, убедился, что так и есть.
До этого, по поведению штатных контролов, а-ля Button, TextView я предполагал, что первична инициализация в конструкторе, а разметка применяется потом. И что просто я что-то не так делаю...
Исходники же как раз и содержат танцы с атрибутами...