Рано или поздно любой разработчик сталкивается с мыслью, что что-то в жизни нужно написать так, чтобы нравилось не заказчику, а самому себе. Решив, что наконец-то я созрел для написания приложения для Android, которым я сам бы пользовался с удовольствием — столкнулся с тем, что внешний вид вьюх в Android не очень-то и готов к воплощению моих красивых идей. И если обычные кнопки и TextView легко поддаются переделке, то вот с TimePicker-ом дела обстоят намного хуже. Творческая часть мозга взбунтовалась и решила, что если в приложении и будет Date/TimePicker, то такой, чтобы им было не только удобно, но и приятно пользоваться, иначе она (та самая творческая часть мозга) объявит бойкот и перестанет помогать.

Ну что же, вызов принят, напишем свой Picker с преферансом и куртизанками.


Именно такой Picker мы будем сооружать.

(Для тех кто просто хочет использовать виджет — ссылка на Git в конце статьи)

Первым делом пришлось ответить себе вопросы — а каким должен быть этот самый Picker и что он должен уметь делать?

Конечно, на вкус и цвет фломастеры разные, но если сравнить стилистику конкурирующих Picker-ов, то на мой взгляд выигрывает правый:


Time picker в Android(слева) и iOS (справа)

Пришлось отвечать для себя — а чем же привлекательнее правый Picker?

Ответ был не один:
  • Приятнее выглядит
  • Интуитивное переключение
  • Никаких лишних элементов


Но настроение хотело большего и поэтому сразу же были добавлены пункты, которые должны присутствовать в новом Picker’e.

  • Возможность легко менять размер без ущерба дизайну
  • Возможность указывать не только время но и… да вообще любую информацию
  • Все-таки должно быть более андроидным нежели яблочным

Итак, цели обозначены, приступим.

Палитра


Вообще все цвета палитры подбирались вручную перед каждым добавлением элемента. Цвета сравнивались и корректировались. В итоге получилась следующая палитра:

Палитра
<color name="datepickerBackground">#ffffff</color>
<color name="datepickerText">#000000</color>
<color name="datepickerSelectedValue">#3770e4</color>
<color name="datepickerSelectedValueShadow">#ffffff</color>
<color name="datapickerGradientStart">#55000000</color>
<color name="datapickerSelectedValueeLineG1">#22ffffff</color>
<color name="datapickerSelectedValueeLineG2">#227d98ff</color>
<color name="datapickerSelectedValueeLineG3">#336585ff</color>
<color name="datapickerSelectedValueeLineG4">#336d8dff</color>
<color name="datapicketSelectedValueBorder">#9a9da4</color>
<color name="datapicketSelectedBorderTop">#f8fcff</color>
<color name="datapicketSelectedBorderBttom">#a1a7bf</color>
<color name="datapickerBlackLines">#000000</color>
<color name="datapickerGrayLines">#cfcdd8</color>


Названия цветов думаю сами за себя говорят, но в любом случае в коде будут пояснения, так что разобраться не составит труда.

Вставляем данный код в наш color.xml.

Создание Picker’а


Picker относится к View Элементам, значит создаем класс DataPicker:

public class DataPicker extends View {
...
}

Нам понадобятся следующие переменные:

Используемые переменные
public Context dataPickercontext=null; //Текущий Context
private OnChangeValueListener mListener=null; //Слушатель нашего события смены значений
public int nowTopPosition = 0; //Позиция скрола
private int minTopPosition = 0; //Минимальная позиция скролла
private int upMaxTopPosition = 0; //Максимальная позиция, в которую может уехать скрол вверх
private int maxTopPosition = 0; //Максимальная позиция скрола внизу
private int maxValueHeight = 0; //Максимальная высота значения
private ArrayList<dpValuesSize> dpvalues = new ArrayList<dpValuesSize>(); //Значения
private int canvasW =0; //Текущая ширина холста
private int canvasH=0; //Текущая высота холста
private int selectedvalueId=0; //Идентификатор выбранного значения
private boolean needAnimation=false; //Включать ли анимацию перемещения
private int needPosition=0; // Нуобходимая позиция
public int valpadding = 30; //Отступ между значениями
private int scrollspeed=0; //Импульсная скорость скролла
private boolean scrolltoup=false; //Движется ли скролл вверх
private  float dpDownY=0; //Координаты клика по холсту с учетом смещения
private float canvasDownY=0; //Координаты точного клика по холсту
private long actdownTime=0; //Момент времени в который был совершен тап по холсту


Определяем конструкторы:

  public DataPicker(Context context) {
        super(context);
        dataPickercontext = context;
    }

    public DataPicker(Context context, AttributeSet attrs) {
        super(context, attrs);
        dataPickercontext = context;
    }

    public DataPicker(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        dataPickercontext = context;
    }

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

Затем нам необходимо переопределить метод onSizeChanged:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
   canvasW = w ;
   canvasH = h ;
   maxValueHeight = (canvasH - (valpadding*2))/2;
   nowTopPosition = 0;
   super.onSizeChanged(w, h, oldw, oldh);
}

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

Следующим этапом необходимо определиться с возможностями нашего Picker’a. Первым делом он должен получать значения, которые необходимо отображать. Создадим для этого метод:

private Handler dpHandler = new Handler();

    public void setValues(final String[] newvalues) {
        if (canvasW == 0 || canvasH == 0) {
            dpHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (canvasW == 0 || canvasH == 0) {
                        dpHandler.postDelayed(this, 100);
                    } else {
                        dpvalues.clear();
                        for (int i = 0; i < newvalues.length; i++) {
                            dpvalues.add(new dpValuesSize(newvalues[i], canvasW, canvasH));
                        }
                    }
                }
            }, 100);
        }
        dpvalues.clear();
        for (int i = 0; i < newvalues.length; i++) {
            dpvalues.add(new dpValuesSize(newvalues[i], canvasW, canvasH));
        }
    }

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

Класс со значениями и их параметрами выглядит так:

class dpValuesSize {
        public int dpWidth = 0; //Ширина нашего текста
        public int dpHeight = 0; //Высота нашего текста
        public String dpValue = ""; //Значения текста
        public int dpTextSize = 0; // Размер шрифта
        public int valpadding = 30; //Отступ между значениями
        public int valinnerLeftpadding = 20; //Отступ по краям у значения

        /*
    Нам необходимо подогнать размер шрифта таким образом, чтобы значение максимально плотно влезало в доверенное ему поле. Грубо говоря - текст должен быть такого размера, чтобы полностью вмещался в наш View, но при этом не вылазил бы за его границы.
    
    Решение на мой взгляд не совсем элегантное. В цикле мы увеличиваем размер шрифта до тех пор, пока он не будет больше нашего поля. Как только размер превышен - останавливаем цикл и берем предыдущее значение.
    Более элегантного алгоритма я не придумал, поэтому буду рад любым идеям и комментариям к данному алгоритму
    */
        public dpValuesSize(String val, int canvasW, int canvasH) {
            try {
                int maxTextHeight = (canvasH - (valpadding * 2)) / 2;
                boolean sizeOK = false;
                dpValue = val;
                while (!sizeOK) {
                    Rect textBounds = new Rect();
                    Paint textPaint = new Paint();
                    dpTextSize++;
                    textPaint.setTextSize(dpTextSize);

                    textPaint.getTextBounds(val, 0, val.length(), textBounds);
                    if (textBounds.width() <= canvasW - (valinnerLeftpadding * 2) && textBounds.height() <= maxTextHeight) {
                        dpWidth = textBounds.width();
                        dpHeight = textBounds.height();
                    } else {
                        sizeOK = true;
                    }

                }
            } catch (Exception e) {
                 e.printStackTrace();
            }
        }
    }

Следующая возможность, которая должна быть — это возможность изменения значения Picker’a путем скролла.

Переопределим для этого метод OnTouch:

    @Override
    public boolean onTouchEvent(MotionEvent motionEvent) {

//Проверяем. Если по холсту прошло событие нажатия, то запоминаем координаты по оси Y для нашего нажатия
        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
            canvasDownY = motionEvent.getY();
            dpDownY = motionEvent.getY() - nowTopPosition;
            needAnimation = false;
            actdownTime = motionEvent.getEventTime();

        }

//Во время скролла по нашему холсту, в переменную nowTopPosition мы записываем величину смещения пальца. Таким обзамо получаем величину, на которую нужно проскроллить наши значения.
        if (motionEvent.getAction() == MotionEvent.ACTION_MOVE) {
            if ((int) (motionEvent.getY() - dpDownY) > maxTopPosition) {
                nowTopPosition = maxTopPosition;
                return true;
            }
            if ((int) (motionEvent.getY() - dpDownY) < upMaxTopPosition) {
                nowTopPosition = upMaxTopPosition;
                return true;
            }
            nowTopPosition = (int) (motionEvent.getY() - dpDownY);
        }

/*Когда палец был убран с холста - нам нужно вычислить к какому значению больше всего соответствует результат скролла, и выровнить значения так, чтобы они попадали строго под наш шаблон (Для этого имеем метод roundingValue().
Далее я дал себе волю немного поэкспериментировать с быстрым скроллингом и добавил переменную scrollspeed. Таким образом, чтобы при быстром перемещении пальца по холсту - у значений создавался запас хода, и значения продолжали скроллиться некоторое время.
*/
        if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
            if (canvasDownY > motionEvent.getY()) {
                scrolltoup = false;
            } else {
                scrolltoup = true;
            }

            if ((motionEvent.getEventTime() - actdownTime < 200) && (Math.abs(dpDownY - motionEvent.getY()) > 100)) {
                scrollspeed = (int) (1000 - (motionEvent.getEventTime() - actdownTime));

            } else {
                scrollspeed = 0;
                roundingValue();
            }
            needAnimation = true;

        }
        return true;
    }

Собственно метод выравнивания наших значений после остановки скроллинга:

    private void roundingValue() {
        //Вычисляем значение переменной needPosition, которая хранит в себе координаты скрола в выровненом значении.
        needPosition = (((nowTopPosition - maxTopPosition - (maxValueHeight / 2)) / (maxValueHeight + valpadding))) * (maxValueHeight + valpadding) + maxTopPosition;
        //Вычисляем идентификатор значения, в котором произойдет остановка скрола
        selectedvalueId = Math.abs(((needPosition - valpadding - (maxValueHeight / 2)) / (maxValueHeight + valpadding)));
        // Сообщаем программисту о том, что некое значение было выбрано.
        onSelected(selectedvalueId);
    }

Как мы видим, в прошлом методе мы использовали функцию onSelected, чтобы наш Picker сообщил о том, что пользователь выбрал какое-то значение.

Создадим для этого слушатель и определим события:

public interface OnChangeValueListener {
        public void onEvent(int valueId);
    }

    public void setOnChangeValueListener(OnChangeValueListener eventListener) {
        mListener = eventListener;
    }

    //Событие, когда было изменено значение
    protected void onSelected(int selectedId) {
        if (mListener != null) {
            mListener.onEvent(selectedId);
        }
    }

    protected void onSelected(int selectedId) {
        if (mListener != null) {
            mListener.onEvent(selectedId);
        }
    }


    //Возвращаем идентификатор выбранного значения
    public int getValueid() {
        try {
            return selectedvalueId;
        } catch (Exception e) {
        }
        return -1;
    }

Когда все основные методы у нас определены, приступим к самому главному. Нам нужно отрисовать наш Picker на холсте. За отрисовку у нас отвечает метод onDraw:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        try {
            //Не будем ничего рисовать, если в Picker’е нет никаких значений
            if (dpvalues.size() == 0) {
                return;
            }
            //Определяем максимальную позицию скрола, в которую можно перемотать наши значения
            upMaxTopPosition = -(((dpvalues.size() - 1) * (maxValueHeight + valpadding)));
            //Очищаем холст (Делаем его прозрачным)
            canvas.drawColor(Color.argb(0, 255, 255, 255));
            //Проверяем, нужно ли анимировать значения (например при перемотке значения были остановлены где-то посередине между двумя значениями)
            if (needAnimation) {
                if (scrollspeed > 0) {
                    scrollspeed -= 30;
                    if (scrolltoup) {
                        int currentPos = nowTopPosition + 30;
                        if ((currentPos) > maxTopPosition) {
                            nowTopPosition = maxTopPosition;
                            scrollspeed = 0;
                            roundingValue();
                        } else {
                            nowTopPosition = currentPos;
                        }
                    }
                    if (!scrolltoup) {
                        int currentPos = nowTopPosition - 30;
                        if ((currentPos) < upMaxTopPosition) {
                            nowTopPosition = upMaxTopPosition;
                            scrollspeed = 0;
                            roundingValue();
                        } else {
                            nowTopPosition = currentPos;
                        }
                    }
                    if (scrollspeed <= 0) {
                        roundingValue();
                    }
                } else {
                    if (nowTopPosition > needPosition) {
                        nowTopPosition -= 20;
                        if (nowTopPosition < needPosition) {
                            nowTopPosition = needPosition;
                        }
                    }
                    if (nowTopPosition < needPosition) {
                        nowTopPosition += 20;
                        if (nowTopPosition > needPosition) {
                            nowTopPosition = needPosition;
                        }
                    }
                    if (nowTopPosition == needPosition) {
                        needAnimation = false;
                    }
                }
            }
            //Вставляем значения
            for (int i = 0; i < dpvalues.size(); i++) {
                try {
                    Paint paint = new Paint();
                    paint.setColor(dataPickercontext.getResources().getColor(R.color.datepickerText));
                    if (selectedvalueId == i) {
                        paint.setColor(dataPickercontext.getResources().getColor(R.color.datepickerSelectedValue));
                        Paint shadowText = new Paint();
                        shadowText.setColor(dataPickercontext.getResources().getColor(R.color.datepickerSelectedValueShadow));
                        shadowText.setTextSize(dpvalues.get(i).dpTextSize);

                        shadowText.setAntiAlias(true);
                        canvas.drawText(dpvalues.get(i).dpValue, (canvasW / 2) - (dpvalues.get(i).dpWidth / 2), ((maxValueHeight + valpadding) * i) + (valpadding + maxValueHeight) + (dpvalues.get(i).dpHeight / 2) + nowTopPosition + 2, shadowText);
                    }
                    paint.setTextSize(dpvalues.get(i).dpTextSize);
                    paint.setAntiAlias(true);

                    canvas.drawText(dpvalues.get(i).dpValue, (canvasW / 2) - (dpvalues.get(i).dpWidth / 2), ((maxValueHeight + valpadding) * i) + (valpadding + maxValueHeight) + (dpvalues.get(i).dpHeight / 2) + nowTopPosition, paint);
                } catch (Exception e) {
                }
            }

Выглядеть это должно вот так:



    //Рисуем боковые границы виджета
    Paint lPBorders = new Paint();
    lPBorders.setColor(dataPickercontext.getResources().getColor(R.color.datapickerBlackLines));
    canvas.drawLine(0,0,0,canvasH,lPBorders);
    canvas.drawLine(1,0,1,canvasH,lPBorders);
    canvas.drawLine(canvasW-1,0,canvasW-1,canvasH,lPBorders);
    canvas.drawLine(canvasW-2,0,canvasW-2,canvasH,lPBorders);
    canvas.drawLine(canvasW,0,canvasW,canvasH,lPBorders);
    lPBorders=new Paint();
    lPBorders.setColor(dataPickercontext.getResources().getColor(R.color.datapickerGrayLines));
    canvas.drawRect(2,0,7,canvasH,lPBorders);
    canvas.drawRect(canvasW-7,0,canvasW-2,canvasH,lPBorders);

Результат:



            //Рисуем затенения
            Paint framePaint = new Paint();
            framePaint.setShader(new LinearGradient(0, 0, 0, getHeight() / 5, dataPickercontext.getResources().getColor(R.color.datapickerGradientStart), Color.TRANSPARENT, Shader.TileMode.CLAMP));
            canvas.drawPaint(framePaint);
            framePaint.setShader(new LinearGradient(0, getHeight(), 0, getHeight() - getHeight() / 5, dataPickercontext.getResources().getColor(R.color.datapickerGradientStart), Color.TRANSPARENT, Shader.TileMode.CLAMP));
            canvas.drawPaint(framePaint);

С тенями уже получше:



//Рисуем полоску веделенного текста
            Path pathSelect = new Path();
            pathSelect.moveTo(0, canvasH / 2 - maxValueHeight / 2 - valpadding / 2);
            pathSelect.lineTo(canvasW, canvasH / 2 - maxValueHeight / 2 - valpadding / 2);
            pathSelect.lineTo(canvasW, canvasH / 2);
            pathSelect.lineTo(0, canvasH / 2);
            pathSelect.lineTo(0, canvasH / 2 - maxValueHeight / 2);
            Paint pathSelectPaint = new Paint();
            pathSelectPaint.setShader(new LinearGradient(0, 0, 0, maxValueHeight / 2, dataPickercontext.getResources().getColor(R.color.datapickerSelectedValueeLineG1), dataPickercontext.getResources().getColor(R.color.datapickerSelectedValueeLineG2), Shader.TileMode.CLAMP));
            canvas.drawPath(pathSelect, pathSelectPaint);

            pathSelect = new Path();
            pathSelect.moveTo(0, canvasH / 2);
            pathSelect.lineTo(canvasW, canvasH / 2);
            pathSelect.lineTo(canvasW, canvasH / 2 + maxValueHeight / 2 + valpadding / 2);
            pathSelect.lineTo(0, canvasH / 2 + maxValueHeight / 2 + valpadding / 2);
            pathSelect.lineTo(0, canvasH / 2);
            pathSelectPaint = new Paint();
            pathSelectPaint.setShader(new LinearGradient(0, 0, 0, maxValueHeight / 2, dataPickercontext.getResources().getColor(R.color.datapickerSelectedValueeLineG3), dataPickercontext.getResources().getColor(R.color.datapickerSelectedValueeLineG4), Shader.TileMode.CLAMP));
            canvas.drawPath(pathSelect, pathSelectPaint);


Уже что-то интересное:



//Рисуем рамку выделенного значения
            Paint selValLightBorder = new Paint();
            Paint selValTopBorder = new Paint();
            Paint selValBottomBorder = new Paint();
            selValLightBorder.setColor(dataPickercontext.getResources().getColor(R.color.datapicketSelectedValueBorder));
            selValTopBorder.setColor(dataPickercontext.getResources().getColor(R.color.datapicketSelectedBorderTop));
            selValBottomBorder.setColor(dataPickercontext.getResources().getColor(R.color.datapicketSelectedBorderBttom));
            canvas.drawLine(0, canvasH / 2 - maxValueHeight / 2 - valpadding / 2, canvasW, canvasH / 2 - maxValueHeight / 2 - valpadding / 2, selValLightBorder);
            canvas.drawLine(0, canvasH / 2 - maxValueHeight / 2 - valpadding / 2 + 1, canvasW, canvasH / 2 - maxValueHeight / 2 - valpadding / 2 + 1, selValTopBorder);
            canvas.drawLine(0, canvasH / 2 + maxValueHeight / 2 + valpadding / 2, canvasW, canvasH / 2 + maxValueHeight / 2 + valpadding / 2, selValLightBorder);
            canvas.drawLine(0, canvasH / 2 + maxValueHeight / 2 + valpadding / 2 - 1, canvasW, canvasH / 2 + maxValueHeight / 2 + valpadding / 2 - 1, selValBottomBorder);

            canvas.drawLine(0, canvasH / 2 - maxValueHeight / 2 - valpadding / 2, 0, canvasH / 2 + maxValueHeight / 2 + valpadding / 2, selValLightBorder);
            canvas.drawLine(1, canvasH / 2 - maxValueHeight / 2 - valpadding / 2, 1, canvasH / 2 + maxValueHeight / 2 + valpadding / 2, selValLightBorder);
            canvas.drawLine(canvasW - 1, canvasH / 2 - maxValueHeight / 2 - valpadding / 2, canvasW - 1, canvasH / 2 + maxValueHeight / 2 + valpadding / 2, selValLightBorder);
            canvas.drawLine(canvasW - 2, canvasH / 2 - maxValueHeight / 2 - valpadding / 2, canvasW - 2, canvasH / 2 + maxValueHeight / 2 + valpadding / 2, selValLightBorder);
            canvas.drawLine(canvasW, canvasH / 2 - maxValueHeight / 2 - valpadding / 2, canvasW, canvasH / 2 + maxValueHeight / 2 + valpadding / 2, selValLightBorder);


Так намного лучше:



            //Рисуем выделенный текст с "тенью"
            Paint selectedTextPaint = new Paint();
            selectedTextPaint.setColor(dataPickercontext.getResources().getColor(R.color.datepickerSelectedValue));
            Paint shadowText = new Paint();
            shadowText.setColor(dataPickercontext.getResources().getColor(R.color.datepickerSelectedValueShadow));
            shadowText.setTextSize(dpvalues.get(selectedvalueId).dpTextSize);
            shadowText.setAntiAlias(true);
            canvas.drawText(dpvalues.get(selectedvalueId).dpValue, (canvasW / 2) - (dpvalues.get(selectedvalueId).dpWidth / 2), ((maxValueHeight + valpadding) * selectedvalueId) + (valpadding + maxValueHeight) + (dpvalues.get(selectedvalueId).dpHeight / 2) + nowTopPosition + 2, shadowText);
            selectedTextPaint.setTextSize(dpvalues.get(selectedvalueId).dpTextSize);
            selectedTextPaint.setAntiAlias(true);
            canvas.drawText(dpvalues.get(selectedvalueId).dpValue, (canvasW / 2) - (dpvalues.get(selectedvalueId).dpWidth / 2), ((maxValueHeight + valpadding) * selectedvalueId) + (valpadding + maxValueHeight) + (dpvalues.get(selectedvalueId).dpHeight / 2) + nowTopPosition, selectedTextPaint);


Идеально:



Завершаем метод onDraw отловом ошибок и устанавливаем fps отрисовки:
        }catch(Exception e){e.printStackTrace();}

        //Перерисовка канваса или FPS. количество кадров прорисовки в секунду
        this.postInvalidateDelayed( 1000 / 60);

    }

Готово!


Удобство нашего Picker’a заключается в том, что мы можем их комбинировать для более удобного выбора значений.
Например, можно скомбинировать 4 компонента для выбора времени напоминания:

Пример готового блюда


Дальше нашему Picker’у можно добавить обработку атрибутов, кастомные параметры, работу с адаптерами… Тут уже поле деятельности неисчерпаемо и в одной статье не разгуляться. Но если сообществу будет интересно продолжение — буду рад продолжить.

Ссылка на Git на готовые исходники.

Комментарии (14)


  1. Rondo
    13.04.2015 18:57
    +5

    UX-дизайнер, который придумал выбирать минуты через Picker, должен гореть в аду — в худшем случае приходится листать 30 элементов, чтобы добраться до нужного. Если делаете Picker — делайте и возможность ввода значений напрямую, пожалуйста.


    1. Moskus
      13.04.2015 20:26
      +1

      Полностью поддерживаю. Пользуюсь двумя приложениями, где в одном picker с прокруткой, в другом — с клавиатурным вводом, как в Jelly Bean. Авторам первого каждый раз хочется оторвать руки.


    1. fiveze
      13.04.2015 20:29
      -2

      В iOS вполне юзабельно, выбор через пикер обходится в пару-тройку свайпов (скроллов), — что по расходам сопоставимо с вводом значений напрямую. Тут реализация похожая, также со свайпами.


  1. jvIlya
    13.04.2015 19:11

    Замечания по коду:

    • до и после знака "=" (а также других знаков операций) должен быть пробел. У вас как придется
    • «act_downTime» — некорректное имя переменной (в вашем случае нижнее подчеркивание лишнее)
    • в try{} блоке у вас довольно много кода, что очень подозрительно
    • "}catch(Exception e){}" — так нельзя делать
    • ваши отладочные сообщения необходимо убирать в релизной версии
    • пустые методы необходимо убрать
    • «1000 / 80» — magic number
    • «try {return dpvalues.get(selectedvalueId).dpValue;} catch (Exception e){}» — не нужно пытаться уместить все в одну строчку
    • по мне так имена членов классов и переменных не очень
    • javadoc не подхватит ваши комментарии при генерации документации
    • расширение у java классов должно быть *.java, а не *.class, как у вас в репозитории
    • чтобы ваша библиотека пользовалась популярностью и ее было удобно использовать, стоит ее опубликовать в maven
    • и т.д.


    А вообще пройдитесь checkstyle, pmd, findbug. Настройте автоформатирования кода в IDE. Посмотрите как оформлены другие библиотеки на github.


    1. tehnolog
      13.04.2015 19:51

      Тоже резануло отсутствие пробелов между "=". В Android Studio над периодически нажимать комбинацию Ctrl+Alt+L


      1. psinetron Автор
        13.04.2015 20:31
        +1

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


        1. DevAndrew
          13.04.2015 22:06
          +1

          Зачем в личку? Я допустим что то интересное для себя подчерпнул из его комментария.


  1. loginsin
    13.04.2015 19:14

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


  1. EndUser
    13.04.2015 20:46

    Не понял, что происходит, когда с 19 накручиваешь 29 часов.

    Что касается эргономики, то наиболее приемлемым суррогатом реальности я считаю увеличение шага минут с 1 до 5 — для большинства задач это адекватно. За исключением таймера. Для таймера всегда лучше перетаскивать стрелку таймера. Очень нечасто у таймера бывают задачи отсчёта более 5 часов (5 оборотов минутной стрелки).

    А ещё лучше в часах просто тянуть две стрелки. Это ИМХО завсегда лучше числовых барабанов.


    1. ahmpro
      13.04.2015 22:09
      +3

      Лучший TimePicker что я видел


      1. Ex3NDR
        13.04.2015 22:57
        +2

        Это и есть стандартный пикер. Не понимаю автора.


        1. psinetron Автор
          14.04.2015 11:23
          +1

          Согласен, это лучший тайм-пикер. И если я не ошибаюсь, данный TimePicker из KitKat. Но разве его можно применить к более ранним версиям андроида?
          К тому же пикер использованный в статье позволяет создать вот такую композицию:

          Вариант DatePickera


    1. psinetron Автор
      14.04.2015 05:43
      +1

      Не понял, что происходит, когда с 19 накручиваешь 29 часов.

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


  1. evnuh
    14.04.2015 00:36

    Про пикер вы подумали, а про то, что кнопка ОК и ОТМЕНА перепутаны местами — нет.