Ну что же, вызов принят, напишем свой 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)
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.tehnolog
13.04.2015 19:51Тоже резануло отсутствие пробелов между "=". В Android Studio над периодически нажимать комбинацию Ctrl+Alt+L
loginsin
13.04.2015 19:14Стиль правого пикера и конечного, конечно, намного интереснее стандартного, но в них есть одна незавершенность, режущая глаз: раз уж предполагается, что надписи расположены на неком вращающемся барабане, то и те надписи, что расположены выше и ниже выбранного элемента, должны в проекции искажаться, но они плоские и весь эффект пространства пропадает.
EndUser
13.04.2015 20:46Не понял, что происходит, когда с 19 накручиваешь 29 часов.
Что касается эргономики, то наиболее приемлемым суррогатом реальности я считаю увеличение шага минут с 1 до 5 — для большинства задач это адекватно. За исключением таймера. Для таймера всегда лучше перетаскивать стрелку таймера. Очень нечасто у таймера бывают задачи отсчёта более 5 часов (5 оборотов минутной стрелки).
А ещё лучше в часах просто тянуть две стрелки. Это ИМХО завсегда лучше числовых барабанов.ahmpro
13.04.2015 22:09+3Лучший TimePicker что я виделEx3NDR
13.04.2015 22:57+2Это и есть стандартный пикер. Не понимаю автора.
psinetron Автор
14.04.2015 11:23+1Согласен, это лучший тайм-пикер. И если я не ошибаюсь, данный TimePicker из KitKat. Но разве его можно применить к более ранним версиям андроида?
К тому же пикер использованный в статье позволяет создать вот такую композицию:
Вариант DatePickera
psinetron Автор
14.04.2015 05:43+1Не понял, что происходит, когда с 19 накручиваешь 29 часов.
Тут уже каждый волен обрабатывать события пикера по своему, так как крутить можно не только время. В примере был добавлен обработчик onSelected. В своем приложении я делаю обработку, и если время не валидно, то смещаю пикер в нужное направление. В вашем примере я делаю проверку. Если при перемотке на 2 первого значения второе значение больше 3х — второе значение отматываю на 0.
evnuh
14.04.2015 00:36Про пикер вы подумали, а про то, что кнопка ОК и ОТМЕНА перепутаны местами — нет.
Rondo
UX-дизайнер, который придумал выбирать минуты через Picker, должен гореть в аду — в худшем случае приходится листать 30 элементов, чтобы добраться до нужного. Если делаете Picker — делайте и возможность ввода значений напрямую, пожалуйста.
Moskus
Полностью поддерживаю. Пользуюсь двумя приложениями, где в одном picker с прокруткой, в другом — с клавиатурным вводом, как в Jelly Bean. Авторам первого каждый раз хочется оторвать руки.
fiveze
В iOS вполне юзабельно, выбор через пикер обходится в пару-тройку свайпов (скроллов), — что по расходам сопоставимо с вводом значений напрямую. Тут реализация похожая, также со свайпами.