суббота, 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 комментариев:

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

    canvas.setMatrix(rotate270matrix);
    super(canvas)

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

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

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

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

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

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

    ОтветитьУдалить
  6. 2 Мур Вотема:
    http://android.git.kernel.org/

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

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

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

    ОтветитьУдалить
  9. Этот комментарий был удален автором.

    ОтветитьУдалить
  10. Здравствуйте, Дарья.

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

    Я затеял 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, а то парсер блога резал весь код...

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

    ОтветитьУдалить
  11. Я так понимаю, конструктор выглядит как-то так:

    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 — уберите эти вызовы. Если нужны захардкоженные значения — всё и так работает. Если нужно, чтобы захардкоженные значения были значениями по умолчанию — придётся городить огород с атрибутами, как рассказано здесь

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

    ОтветитьУдалить

Пожалуйста, пишите содержательные комментарии и соблюдайте вежливость.