Планирование
- Слайдер — элемент пользовательского интерфейса, позволяющий регулировать какие-либо численные параметры в некотором интервале и служащий альтернативой текстовым полям. Преимущества слайдера — в удобстве настройки.
- Поставим целью написать (полный код класса и исходники приложения с его использованием находятся в конце статьи) слайдер (ползунок) на android со следующей функциональностью (регуляторы перемещаются сенсорно):
- -можно было задавать интервал, ограниченный двумя значениями
- -несмотря на это, был бы доступен режим обычного слайдера с отображением только одного установщика (например, для регулирования громкости)
- -была шкала значений внизу с регулируемым шагом
- -возможна к=активация режима привязки регуляторов к круглым значениям
- -отображались бы установленные значения над установщиками
- -возможность задания заголовка (Title)
- -гибкая настраиваемость стилей всех элементов
Делаем каркас класса
- Прежде всего, создаем новый класс и делаем все как в этой статье (пишем конструкторы, основные методы класса и помещаем экземпляр класса в main.xml), оставив в методе onDraw() только вызов конструктора предка (первая строка).
- Определим все поля нашего класса — параметры слайдера:
- Наглядное объяснение смысла непрокомментированных параметров (значения некоторых параметров в коде выше не совпадают с их значениями на картинке — например, заголовок красный, а не белый — поскольку они были изменены в коде Activity после создания компонента):
- В Activity, после создания компонента, нужно будет выполнить метод инициализации, в котором мы определим все внутренние параметры слайдера. Код метода:
- Вызов метода initialization в коде Activity:
- slider.initialization(12, -5, 3, 10, false, R.drawable.draw, 5);
public class ValueSetter extends View{ private double maxValue; private double minValue; private double beginValue; private double endValue; private boolean isOneSlider;//Если true – один ползунок, иначе - два private boolean isSnape;//Активирован ли режим привязки private double snapeInterval;//Расстояние между точками привязки private double snapeDistantion;//Расстояние привязки private Bitmap sliderBitmap; Paint paint=new Paint(); private int regularColor=0x66FF0000; private int tapeColor=0x6623FF32; private String title="Super cute slider"; private int titleTextColor=Color.RED;//цвет заголовка private double titleTextSize=24; private int regularTextColor=Color.WHITE;//цвет всех остальных надписей private double regularTextSize=15; private int regularTextSections=4; private float tapeLeft; private float tapeRight; private float tapeTop; private float tapeBottom; private static final double TAPE_HEIGHT_PERCENTAGE=0.08; private static final double TAPE_Y_PERCENTAGE=0.46; private static final double TAPE_WIDTH_PERCENTAGE=0.8; private static final double WIDTH_DIVIDE_HEIGHT_OF_MONOSPACE_SYMBOL=0.61;//Отношение высоты символа моноширинного текста к его длине private static final double TOP_OF_TITLE_TEXT=0.2; private static final double REGULAR_TEXT_OFFSET=1.5;//расстояние в размерах кегля регулярного текста от нижнего конца линейки до нижней граница надписей под ней … }
public void initialization(double maxValue, double minValue, double beginValue, double endValue, boolean isOneSlider, int sliderBitmap, int regularTextSections) { this.beginValue=beginValue; this.endValue=endValue; if(maxValue>minValue){ this.maxValue=maxValue; this.minValue=minValue; } else{//Если пользователь перепутал порядок следования этих параметров this.maxValue=minValue; this.minValue=maxValue; } this.isOneSlider=isOneSlider; this.sliderBitmap=BitmapFactory.decodeResource(getResources(), sliderBitmap);//Превращаем в Bitmap изображение регулятора из ресурсов this.isSnape=false; this.regularTextSections=regularTextSections; }
Программное рисование
- Начнем писать метод onDraw().
- Прежде всего, чтобы не было ошибок, немного перестрахуемся:
- Определим координаты границ полосы слайдера:
- tapeLeft=(float) (width*(1-TAPE_WIDTH_PERCENTAGE)/2);
-
- Нарисуем два прямоугольника: первый будет ограничивать всю полосу, а другой — только выбираемый пользователем интервал на ней. Тогда имеет смысл написать отдельный метод, который будет определять координату x точки, которая соответствует значению переменной (на основании координат границ участка и максимального/минимального значения на полосе), поскольку нужно будет найти левый и правый край второго прямоугольника:
-
-
- Теперь нам ничто не мешает нарисовать 2 прямоугольника, составляющие полосу слайдера:
-
-
- Выведем регуляторы. Но для начала нам придется определить их длину и ширину для того, чтобы поместить их ровно на середину полоски ровно на нужное значение:
-
-
- Пришло время программно выводить надписи. При вызове метода canvas.drawText(text, x, y) мы указываем левый-нижний край текста. Нам же хочется, чтобы они все выводились симметрично относительно вычисленной точки (выравнивание по центру). Например, ровно половина текста заголовка — по длине - находилась бы слева от него, а ровно половина — справа.
- Нам придется для этого специально смещать положение текста ровно на половины его длины влево. Для этого:
- -все надписи должны иметь моноширинный шрифт
- -придется написать специальный метод, возвращающий исправленную координаты x текста как функцию от его длины, размера шрифта и старого значения x. Для реализации этого метода понадобится константа WIDTH_DIVIDE_HEIGHT_OF_MONOSPACE_SYMBOL, значение который было найдено экспериментально. Вот он (поскольку результат его выполнения не зависит от класса, его вызвавшего, можно сделать его статическим):
-
public static double getTrueTextLeft(String text, double textLeft, double textSize){ return textLeft-text.length()*textSize*WIDTH_DIVIDE_HEIGHT_OF_MONOSPACE_SYMBOL/2; }
- Можно программировать создание надписей:
-
paint.setTypeface(Typeface.MONOSPACE); paint.setColor(this.getTitleTextColor()); paint.setTextSize((float) this.getTitleTextSize()); //Выводим Title canvas.drawText(getTitle(), (float) getTrueTextLeft(getTitle(), width/2, getTitleTextSize()), (float)(height*this.TOP_OF_TITLE_TEXT), paint); paint.setColor(this.getRegularTextColor()); paint.setTextSize((float) this.getRegularTextSize()); //Выводим значения внизу полосы for (int i=0;i<=regularTextSections+1;i++){ double x=tapeLeft+(tapeRight-tapeLeft)/(regularTextSections)*(double)(i); String text=round(getValue(x), 3)+""; canvas.drawText(text, (float) getTrueTextLeft(text, x, getRegularTextSize()), (float) (tapeBottom+getRegularTextSize()*REGULAR_TEXT_OFFSET), paint); } double beginX=getRenderingX(beginValue); double endX=getRenderingX(endValue); String beginText=round(beginValue,3)+""; String endText=round(endValue,3)+""; //Выводим подписи значений регуляторов if(!isOneSlider){ canvas.drawText(beginText, (float) getTrueTextLeft(beginText, beginX, getRegularTextSize()), (float)(tapeTop-getRegularTextSize()*REGULAR_TEXT_OFFSET), paint); } canvas.drawText(endText, (float) getTrueTextLeft(endText, endX, getRegularTextSize()), (float)(tapeTop-getRegularTextSize()*REGULAR_TEXT_OFFSET), paint);
-
- Из всего этого кода мы еще не определили методы getValue(double x), который находит значение на полосе слайдера, соответствующее определенному значению координаты x (таким образом, этот метод является обратным к getRenderightX()) и round(double a, int d) – округляет до d-го знака:
private double getValue(double renderingX) { double tapeWidthInPixels=tapeRight-tapeLeft; double percentOfTape=(renderingX-tapeLeft)/tapeWidthInPixels; //Проверки на выход за пределы полосы: if(percentOfTape<0){ percentOfTape=0; } if(percentOfTape>1){ percentOfTape=1; } return minValue+(maxValue-minValue)*percentOfTape; } private double round(double value, int d) { return Math.round(value*Math.pow(10, d))/(double)(Math.pow(10, d)); }
Обработка событий касания компонента
- Обработаем прикосновения пользователя:
public boolean onTouchEvent(MotionEvent event){ double x=event.getX(); //Определяем координаты регуляторов double xOfBeginSlider=getRenderingX(beginValue); double xOfEndSlider=getRenderingX(endValue); boolean sliderInTouch=true;//Если true, к месту перемещать в место прикосновения надо правый ползунок, иначе — левый if(!isOneSlider){ if(Math.abs(xOfBeginSlider-x)<Math.abs(xOfEndSlider-x)){ sliderInTouch=false; } } //Изменяем соответствующую переменную if(sliderInTouch){ endValue=getValue(x); } else{ beginValue=getValue(x); } invalidate(); return true; }
if(isOneSlider){//Если слайдер односторонний, значение первой переменной всегда должно быть минимально this.beginValue=this.minValue; } if(beginValue>endValue){//Если такое случится (а это возможно при привязке или пользовательской установке), приводим все в норму double temp=endValue; endValue=beginValue; beginValue=temp; }
tapeLeft=(float) (width*(1-TAPE_WIDTH_PERCENTAGE)/2); tapeRight=(float) (width*(TAPE_WIDTH_PERCENTAGE+(1-TAPE_WIDTH_PERCENTAGE)/2)); tapeTop=(float) (height*TAPE_Y_PERCENTAGE); tapeBottom=(float(height*(TAPE_Y_PERCENTAGE+TAPE_HEIGHT_PERCENTAGE));
private float getRenderingX(double value) { double tapeWidthInPixels=tapeRight-tapeLeft; double percentOfTape=(value-minValue)/(maxValue-minValue); return (float) (percentOfTape*tapeWidthInPixels+tapeLeft); }
paint.setColor(getRegularColor()); canvas.drawRect(tapeLeft, tapeTop, tapeRight, tapeBottom, paint); paint.setColor(getTapeColor()); canvas.drawRect(getRenderingX(beginValue), tapeTop, getRenderingX(endValue), tapeBottom, paint);
double sliderWidth; double sliderHeight; sliderWidth=sliderBitmap.getWidth(); sliderHeight=sliderBitmap.getHeight(); float sliderTop; float sliderLeftDifference;//Смещение верхнего-левого угла слайдера влево, чтобы его середина была на значении sliderTop=(float) ((tapeTop+tapeBottom)/2-sliderHeight/2); sliderLeftDifference=(float) (sliderWidth/2); paint.setColor(Color.BLACK);//Их-ха прошлых манипуляций paint имеет прозрачность. Которая нам не нужна canvas.drawBitmap(sliderBitmap, getRenderingX(endValue)-sliderLeftDifference, sliderTop, paint); if(!isOneSlider){ canvas.drawBitmap(sliderBitmap, getRenderingX(beginValue)-sliderLeftDifference, sliderTop, paint); }
Привязка к значениям
- Слайдер почти закончен. Запрограммируем привязку к значениям. Активировать режим привязки (по умолчанию отключенный) можно будет методом setSnape(), за привязку будет отвечать метод snape в виде такой конструкции в onDraw()
... if(isSnape){ beginValue=snape(beginValue); endValue=snape(endValue); } … //Код методов snape() и setSnape(): public double snape(double value) { double closestSnapeValue=Math.round(value/snapeInterval)*snapeInterval; if(closestSnapeValue>=minValue && closestSnapeValue<=maxValue){ if(Math.abs(value-closestSnapeValue)<=snapeDistantion){ return closestSnapeValue; } } return value; } public void setSnape(double interval, double distantion){ isSnape=true; snapeInterval=interval; snapeDistantion=distantion; }
-
- Теперь набросаем методов для установки и чтения приватных свойств, чтобы с классом был удобнее работать:
-
Остальные методы
public double getValue(boolean number) {//Перегрузим if(number){ return beginValue; } else{ return endValue; } } public void setValue(boolean number, double value) { if(number){ beginValue=value; } else{ endValue=value; } invalidate(); } public void setTitle(String title) { invalidate(); this.title = title; } public String getTitle() { return title; } public void setTitleTextColor(int titleTextColor) { invalidate(); this.titleTextColor = titleTextColor; } public int getTitleTextColor() { return titleTextColor; } public void setTitleTextSize(double titleTextSize) { invalidate(); this.titleTextSize = titleTextSize; } public double getTitleTextSize() { return titleTextSize; } public void setRegularTextSize(double regularTextSize) { invalidate(); this.regularTextSize = regularTextSize; } public double getRegularTextSize() { return regularTextSize; } public void setRegularTextColor(int regularTextColor) { invalidate(); this.regularTextColor = regularTextColor; } public int getRegularTextColor() { return regularTextColor; } public void setRegularColor(int regularColor) { invalidate(); this.regularColor = regularColor; } public int getRegularColor() { return regularColor; } public void setTapeColor(int tapeColor) { this.tapeColor = tapeColor; invalidate(); } public int getTapeColor() { return tapeColor; }
Полный код класса
-
package com.slider_test; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; public class ValueSetter extends View{ private double maxValue; private double minValue; private double beginValue; private double endValue; private boolean isOneSlider; private boolean isSnape; private double snapeInterval; private double snapeDistantion; private Bitmap sliderBitmap; Paint paint=new Paint(); private int regularColor=0x66FF0000; private int tapeColor=0x6623FF32; private String title="Super cute slider"; private int titleTextColor=Color.RED; private double titleTextSize=24; private int regularTextColor=Color.WHITE; private double regularTextSize=15; private int regularTextSections=4; private float tapeLeft; private float tapeRight; private float tapeTop; private float tapeBottom; public int height; public int width; private static final double TAPE_HEIGHT_PERCENTAGE=0.08; private static final double TAPE_Y_PERCENTAGE=0.46; private static final double TAPE_WIDTH_PERCENTAGE=0.8; private static final double HEIGHT_DIVIDE_WIDTH_OF_MONOSPACE_SYMBOL=0.61; private static final double TOP_OF_TITLE_TEXT=0.2; private static final double REGULAR_TEXT_OFFSET=1.5; public static double getTrueTextLeft(String text, double textLeft, double textSize){ return textLeft-text.length()*textSize*HEIGHT_DIVIDE_WIDTH_OF_MONOSPACE_SYMBOL/2; } public void initialization(double maxValue, double minValue, double beginValue, double endValue, boolean isOneSlider, int sliderBitmap, int regularTextSections) { this.beginValue=beginValue; this.endValue=endValue; if(maxValue>minValue){ this.maxValue=maxValue; this.minValue=minValue; } else{ this.maxValue=minValue; this.minValue=maxValue; } this.isOneSlider=isOneSlider; this.sliderBitmap=BitmapFactory.decodeResource(getResources(), sliderBitmap); this.isSnape=false; this.regularTextSections=regularTextSections; } public ValueSetter(Context context, AttributeSet attrs){ super(context, attrs); } public ValueSetter(Context context){ super(context); } @Override protected void onMeasure(int widthSpecId, int heightSpecId){ this.height = View.MeasureSpec.getSize(heightSpecId); this.width = View.MeasureSpec.getSize(widthSpecId); setMeasuredDimension(width, height); } protected void onDraw(Canvas canvas){ super.onDraw(canvas); if(isOneSlider){ this.beginValue=this.minValue; } if(isSnape){ beginValue=snape(beginValue); endValue=snape(endValue); } if(beginValue>endValue){ double temp=endValue; endValue=beginValue; beginValue=temp; } tapeLeft=(float) (width*(1-TAPE_WIDTH_PERCENTAGE)/2); tapeRight=(float) (width*(TAPE_WIDTH_PERCENTAGE+(1-TAPE_WIDTH_PERCENTAGE)/2)); tapeTop=(float) (height*TAPE_Y_PERCENTAGE); tapeBottom=(float) (height*(TAPE_Y_PERCENTAGE+TAPE_HEIGHT_PERCENTAGE)); paint.setColor(getRegularColor()); canvas.drawRect(tapeLeft, tapeTop, tapeRight, tapeBottom, paint); paint.setColor(getTapeColor()); canvas.drawRect(getRenderingX(beginValue), tapeTop, getRenderingX(endValue), tapeBottom, paint); double sliderWidth; double sliderHeight; sliderWidth=sliderBitmap.getWidth(); sliderHeight=sliderBitmap.getHeight(); float sliderTop; float sliderLeftDifference; sliderTop=(float) ((tapeTop+tapeBottom)/2-sliderHeight/2); sliderLeftDifference=(float) (sliderWidth/2); paint.setColor(Color.BLACK); canvas.drawBitmap(sliderBitmap, getRenderingX(endValue)-sliderLeftDifference, sliderTop, paint); if(!isOneSlider){ canvas.drawBitmap(sliderBitmap, getRenderingX(beginValue)-sliderLeftDifference, sliderTop, paint); } paint.setTypeface(Typeface.MONOSPACE); paint.setColor(this.getTitleTextColor()); paint.setTextSize((float) this.getTitleTextSize()); canvas.drawText(getTitle(), (float) getTrueTextLeft(getTitle(), width/2, getTitleTextSize()), (float)(height*this.TOP_OF_TITLE_TEXT), paint); paint.setColor(this.getRegularTextColor()); paint.setTextSize((float) this.getRegularTextSize()); for (int i=0;i<=regularTextSections+1;i++){ double x=tapeLeft+(tapeRight-tapeLeft)/(regularTextSections)*(double)(i); String text=round(getValue(x), 3)+""; canvas.drawText(text, (float) getTrueTextLeft(text, x, getRegularTextSize()), (float)(tapeBottom+getRegularTextSize()*REGULAR_TEXT_OFFSET), paint); } double beginX=getRenderingX(beginValue); double endX=getRenderingX(endValue); String beginText=round(beginValue,3)+""; String endText=round(endValue,3)+""; //Log.d("app", beginX+" "+endX); if(!isOneSlider){ canvas.drawText(beginText, (float) getTrueTextLeft(beginText, beginX, getRegularTextSize()), (float)(tapeTop-getRegularTextSize()*REGULAR_TEXT_OFFSET), paint); } canvas.drawText(endText, (float) getTrueTextLeft(endText, endX, getRegularTextSize()), (float)(tapeTop-getRegularTextSize()*REGULAR_TEXT_OFFSET), paint); } public double snape(double value) { double closestSnapeValue=Math.round(value/snapeInterval)*snapeInterval; if(closestSnapeValue>=minValue && closestSnapeValue<=maxValue){ if(Math.abs(value-closestSnapeValue)<=snapeDistantion){ return closestSnapeValue; } } return value; } private double round(double value, int d) { return Math.round(value*Math.pow(10, d))/(double)(Math.pow(10, d)); } private float getRenderingX(double value) { double tapeWidthInPixels=tapeRight-tapeLeft; double percentOfTape=(value-minValue)/(maxValue-minValue); return (float) (percentOfTape*tapeWidthInPixels+tapeLeft); } private double getValue(double renderingX) { double tapeWidthInPixels=tapeRight-tapeLeft; double percentOfTape=(renderingX-tapeLeft)/tapeWidthInPixels; if(percentOfTape<0){ percentOfTape=0; } if(percentOfTape>1){ percentOfTape=1; } return minValue+(maxValue-minValue)*percentOfTape; } public boolean onTouchEvent(MotionEvent event){ double x=event.getX(); double xOfBeginSlider=getRenderingX(beginValue); double xOfEndSlider=getRenderingX(endValue); boolean sliderInTouch=true; if(!isOneSlider){ if(Math.abs(xOfBeginSlider-x)<Math.abs(xOfEndSlider-x)){ sliderInTouch=false; } } if(sliderInTouch){ endValue=getValue(x); } else{ beginValue=getValue(x); } invalidate(); return true; } public double getValue(boolean number) { if(number){ return beginValue; } else{ return endValue; } } public void setValue(boolean number, double value) { if(number){ beginValue=value; } else{ endValue=value; } invalidate(); } public void setSnape(double interval, double distantion){ isSnape=true; snapeInterval=interval; snapeDistantion=distantion; } public void setTitle(String title) { invalidate(); this.title = title; } public String getTitle() { return title; } public void setTitleTextColor(int titleTextColor) { invalidate(); this.titleTextColor = titleTextColor; } public int getTitleTextColor() { return titleTextColor; } public void setTitleTextSize(double titleTextSize) { invalidate(); this.titleTextSize = titleTextSize; } public double getTitleTextSize() { return titleTextSize; } public void setRegularTextSize(double regularTextSize) { invalidate(); this.regularTextSize = regularTextSize; } public double getRegularTextSize() { return regularTextSize; } public void setRegularTextColor(int regularTextColor) { invalidate(); this.regularTextColor = regularTextColor; } public int getRegularTextColor() { return regularTextColor; } public void setRegularColor(int regularColor) { invalidate(); this.regularColor = regularColor; } public int getRegularColor() { return regularColor; } public void setTapeColor(int tapeColor) { this.tapeColor = tapeColor; invalidate(); } public int getTapeColor() { return tapeColor; } }
- Исходники приложения с использование класса можно скачать отсюда
Комментариев нет:
Отправить комментарий