Показаны сообщения с ярлыком Canvas. Показать все сообщения
Показаны сообщения с ярлыком Canvas. Показать все сообщения

пятница, 26 августа 2011 г.

Android: делаем холст для рисования

Допустим вы решили сделать маленький Paint для Android. Например, для развития художественных талантов у своего ребёнка :). Основой такого приложения будет холст: некая View, которая может "ощущать" прикосновения и отображать на себе путь пальца. Для реализации таких функций создаём класс, расширяющий View и переопределяем у него два метода: onTouchEvent (позволит перехватывать движения пальца) и onDraw (позволит рисовать).

  1. public class PaintView extends View {
  2.  
  3.   
  4.   private int linecolor, alpha, lwidth;
  5.   private int WIDTH;
  6.   private int HEIGHT;
  7.   MainActivity act;
  8.  
  9.   public PaintView(MainActivity act, int linecolor, int alpha, int lwidth, int width, int height) {
  10.     super(act);
  11.     this.act = act;
  12.     this.linecolor = linecolor;
  13.     this.alpha = alpha;
  14.     this.WIDTH = width;
  15.     this.HEIGHT = height;
  16.     this.lwidth = lwidth;
  17.   }
  18.   
  19.   @Override
  20.   public boolean onTouchEvent(MotionEvent e) {
  21.  
  22.     int action = e.getAction();
  23.  
  24.     if (action == MotionEvent.ACTION_UP) {
  25.       act.addPoint(null);
  26.  
  27.     } else if (action == MotionEvent.ACTION_MOVE || action == MotionEvent.ACTION_DOWN) {
  28.       Point point = new Point(Math.round(e.getX()), Math.round(e.getY()), linecolor, alpha, lwidth);
  29.       act.addPoint(point);
  30.       postInvalidate();
  31.     }
  32.  
  33.     return true;
  34.   }
  35.  
  36.   @Override
  37.   protected void onDraw(Canvas c) {
  38.     Point curr = null;
  39.     for (Point data : act.getPoints()) {
  40.       if (curr != null && data != null) {
  41.         Paint paint = new Paint();
  42.         paint.setColor(data.getColor());
  43.         paint.setAlpha(data.getAlpha());
  44.         if (data.getWidth()==1) {
  45.           c.drawLine(curr.getX(), curr.getY(), data.getX(), data.getY(), paint);
  46.         } else {
  47.           c.drawRect(curr.getX(), curr.getY(), data.getX()+data.getWidth(), data.getY()+data.getWidth(), paint);
  48.         }
  49.       }
  50.       curr = data;
  51.     }
  52.   }
  53.  
  54.   @Override
  55.   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  56.     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  57.     this.setMeasuredDimension(WIDTH, HEIGHT);
  58.   }
  59. }
Рассмотрим подробнее метод onTouchEvent. Для событий прикосновения и перемещения мы получаем координаты точки на экране и создаём экземпляр Point, добавляя его в ArrayList с помощью метода addPoint(point) в MainActivity. Почему не хранить коллекцию точек прямо в классе? Так проще обрабатывать события, требующие перерисовки холста. Просто создаём новый холст, а данные для отрисовки он возьмёт в MainActivity.
Класс Point выглядит просто:
public class Point {
  private int x, y, color, alpha, width;

  public Point(int x, int y, int color, int alpha, int width) {
    this.x = x;
    this.y = y;
    this.color = color;
    this.alpha = alpha;
    this.width = width;
  }

  public int getX() {
    return x;
  }

  public int getY() {
    return y;
  }

  public int getColor() {
    return color;
  }

  public int getAlpha() {
    return alpha;
  }

  public int getWidth() {
    return width;
  }
}

...это обычное хранилище координат точки и её характеристик.
Вызываем наш холст из MainActivity следующим образом:
PaintView paint = new PaintView(this, initColor, alpha, linew, w, h);
Тут:
this - экземпляр MainActivity, из которого, собственно делаем вызов
initColor, alpha, linew - настройки цвета, прозрачности и толщины линии
w и h - ширина и высота доступной для рисования области в пикселях. Получаем её так:
    Resources res = getResources();
    int h = res.getDisplayMetrics().heightPixels;
    int w = res.getDisplayMetrics().widthPixels;

Созданный экземпляр холста может быть добавлен в нашу структуру View с помощью метода addView родительского контейнера а контейнер верхнего уровня устанавливаем как контент окна методом setContentView.
Останется только добавить в наше приложение панель с кнопками для выбора цвета, толщины линий и т.п. и простенькая "рисовалка" готова :).

среда, 6 июля 2011 г.

Строим диаграмму в Android-приложении

Допустим, вы решили сделать виджет, который будет отображать текущий курс валют. Вы идёте на любой открытый источник, берёте данные и выводите клиенту. Ничего сложного. Но и ничего особенного - таких виджетов миллион. Вы идёте дальше: принимаете решение хранить полученные данные за предыдущие дни и отображать клиенту информацию в виде диаграммы. Вот тут-то мы и сталкиваемся с реализацией 2D-графики в Android.
Чтобы создать объект изображения, нужно переопределить View. Назовём наш объект ChartView:

  1. public class ChartView extends View {
  2.   private int width = 10;
  3.   ArrayList<ChartItem> charts;
  4.   int max = 0;
  5.   ShapeDrawable background;
  6.   
  7.   public ChartView(Context context, ArrayList<ChartItem> charts, int top, int bgColor) {
  8.     super(context);
  9.     Resources res = getResources();
  10.     int ws = res.getDisplayMetrics().widthPixels;
  11.     
  12.     ArrayList<ChartItem> gencharts = new ArrayList<ChartItem>();
  13.     for (ChartItem ci : charts) {
  14.       int[] points = scaleTo(ci.getPoints(), top);
  15.       ArrayList<ShapeDrawable> myDraws = new ArrayList<ShapeDrawable>();
  16.       for (int i=0; i<points.length; i++) {
  17.         int h = points[i];
  18.         ShapeDrawable mDrawable = new ShapeDrawable(new RectShape());
  19.         mDrawable.getPaint().setColor(ci.getColor());
  20.         mDrawable.getPaint().setAlpha(ci.getAlpha());
  21.         mDrawable.setBounds(width*i, top-h, width*(i+1), top);
  22.         mDrawable.getPaint().setShader(new LinearGradient(width*i, top-h, width*(i+1), top-h, ci.getColor(), Color.WHITE, Shader.TileMode.REPEAT));
  23.         myDraws.add(mDrawable);
  24.       }
  25.       ci.setDraws(myDraws);
  26.       gencharts.add(ci);
  27.     }
  28.     this.charts = gencharts;
  29.     background = new ShapeDrawable(new RectShape());
  30.     background.setBounds(0, 0, ws, top);
  31.     background.getPaint().setColor(bgColor);
  32.   }
  33.  
  34.   @Override
  35.   protected void onDraw(Canvas canvas) {
  36.     background.draw(canvas);
  37.     for (ChartItem ci : charts) {
  38.       ArrayList<ShapeDrawable> draws = ci.getDraws();
  39.       for (int i=0; i<draws.size(); i++) {
  40.         ShapeDrawable d = draws.get(i);
  41.         d.draw(canvas);
  42.       }
  43.     }
  44.   }
  45.   
  46.   private int getMax(int[] ii) {
  47.     for (int i : ii) {
  48.       if (max<i) max = i;
  49.     }
  50.     return max;
  51.   }
  52.   
  53.   private int[] scaleTo(int[] ii, int vmax) {
  54.     int[] scaled = new int[ii.length];
  55.     double k = (double) vmax / getMax(ii);
  56.     for(int i=0; i<ii.length; i++) {
  57.       scaled[i] = (int) (ii[i]*k);
  58.     }
  59.     return scaled;
  60.   }
  61. }
Рассмотрим наш класс подробнее.
В конструктор обязательно нужно передавать Context - его мы сразу же отдадим в конструктор предка. Кроме этого в конструктор мы передаём массив объектов ChartItem, которые представляют данные для каждой из диаграмм, которые мы собираемся построить на нашем изображении. Следующие два параметра общие для всех диаграмм на изображении: максимальная высота и фон. 
В строках 9-10 получаем текущую ширину экрана, чтобы полностью закрасить её фоном. 
Все графические примитивы создаются как объекты ShapeDrawable. В конструкторе мы устанавливаем параметры для прямоугольников, составляющих столбцы диаграмм: ширину, высоту, цвет. Делаем их симпатичнее с помощью градиента (строка 22). Все созданные объекты должны быть нарисованы на канве, для чего переопределяем метод protected void onDraw(Canvas canvas) и в нём для всех ShapeDrawable вызываем метод draw(canvas) (строки 35-44). Делать это нужно в правильном порядке: следующий объект будет рисоваться поверх предыдущего. 
Определённая сложность есть в расчёте высоты столбцов диаграммы: нужно чтобы они поместились в максимальную высоту и при этом сохранили пропорции. Это решаем с помощью метода  private int[] scaleTo(int[] ii, int vmax) в который передаём массив данных для конкретной диаграммы и максимальную высоту. На выходе получаем уже масштабированный массив (строки 53-60). 
Добавляя в setContentView вашего Activity созданный экземпляр СhartView, получаете результат вроде того, что на картинке. В данном случае выбран серый фон и две диаграммы с прозрачностью 50% синего и зелёного цвета.