Translate

пятница, 13 июля 2012 г.

Программирование под android. Создание двустороннего слайдера.





Планирование


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



-можно было задавать интервал, ограниченный двумя значениями
-несмотря на это, был бы доступен режим обычного слайдера с отображением только одного установщика (например, для регулирования громкости)
-была шкала значений внизу с регулируемым шагом
-возможна к=активация режима привязки регуляторов к круглым значениям
-отображались бы установленные значения над установщиками
-возможность задания заголовка (Title)
-гибкая настраиваемость стилей всех элементов





Делаем каркас класса

Прежде всего, создаем новый класс и делаем все как в этой статье  (пишем конструкторы, основные методы класса и помещаем экземпляр класса в main.xml), оставив в методе onDraw() только вызов конструктора предка (первая строка).


Определим все поля нашего класса — параметры слайдера:

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;//расстояние в размерах кегля регулярного текста от нижнего конца линейки до нижней граница надписей под ней
    …
}

Наглядное объяснение смысла непрокомментированных параметров (значения некоторых параметров в коде выше не совпадают с их значениями на картинке — например, заголовок красный, а не белый — поскольку они были изменены в коде Activity после создания компонента):





В Activity, после создания компонента, нужно будет выполнить метод инициализации, в котором мы определим все внутренние параметры слайдера. Код метода:



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;
}

Вызов метода initialization в коде Activity:

slider.initialization(12, -5, 3, 10, false, R.drawable.draw, 5);



Программное рисование

Начнем писать метод onDraw().

Прежде всего, чтобы не было ошибок, немного перестрахуемся:

if(isOneSlider){//Если слайдер односторонний, значение первой переменной всегда должно быть минимально
                this.beginValue=this.minValue;
}
          
if(beginValue>endValue){//Если такое случится (а это возможно при привязке или пользовательской установке), приводим все в норму
                 double temp=endValue;
                 endValue=beginValue;
                 beginValue=temp;
}

  Определим координаты границ полосы слайдера:

tapeLeft=(float) (width*(1-TAPE_WIDTH_PERCENTAGE)/2);

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));


   Нарисуем два прямоугольника: первый будет ограничивать всю полосу, а другой — только выбираемый пользователем интервал на ней. Тогда имеет смысл написать отдельный метод, который будет определять координату x точки, которая соответствует значению переменной (на основании координат границ участка и максимального/минимального значения на полосе), поскольку нужно будет найти левый и правый край второго прямоугольника:


private float getRenderingX(double value) {
    double tapeWidthInPixels=tapeRight-tapeLeft;
    double percentOfTape=(value-minValue)/(maxValue-minValue);
       
    return (float) (percentOfTape*tapeWidthInPixels+tapeLeft);
}




Теперь нам ничто не мешает нарисовать 2 прямоугольника, составляющие полосу слайдера:

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);
}



Пришло время программно выводить надписи. При вызове метода 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;

}
 
 
 
 

Привязка к значениям


Слайдер почти закончен. Запрограммируем привязку к значениям. Активировать режим привязки (по умолчанию отключенный) можно будет методом 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;
    }
}


Исходники приложения с использование класса можно скачать отсюда







Комментариев нет:

Отправить комментарий

Related Posts Plugin for WordPress, Blogger...