пятница, 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.
Останется только добавить в наше приложение панель с кнопками для выбора цвета, толщины линий и т.п. и простенькая "рисовалка" готова :).

17 комментариев:

  1. С какого перепугу цвет (color)задается как целое (int?
    Зачем нужна переменная initColor если в PaintView она нигде не используется?

    ОтветитьУдалить
  2. А содержание MainActivity увидеть можно?

    ОтветитьУдалить
  3. Что представляет собой метод getPoints() ?
    Буду очень благодарен за ответы

    ОтветитьУдалить
  4. В MainActivity содержится массив точек изображения в виде ArrayList. Метод act.addPoint(Point point) добавляет точку в ArrayList а метод act.getPoints() возвращает ArrayList всех точек.

    ОтветитьУдалить
  5. Этот комментарий был удален автором.

    ОтветитьУдалить
  6. Этот комментарий был удален автором.

    ОтветитьУдалить
  7. Этот комментарий был удален автором.

    ОтветитьУдалить
  8. СПАСИБО БОЛЬШОЕ!!!

    Теперь буду думать над методом "ластик".

    ОтветитьУдалить
  9. Ещё один нубский вопрос:
    Как изменять цвет области рисования?

    Спасибо

    ОтветитьУдалить
  10. Сам себе отвечу:
    Методом setBackgroundColor() класса View

    Автору ещё раз большое спасибо!

    ОтветитьУдалить
  11. Здравствуйте!
    Реализуя данное чудо столкнулся с проблемой сохранения прорисованного в файл.
    При попытке хотябы создать bitmap программа "ложиться".
    Ниже привожу метод сожранения картинки:

    private static void SavePicture(PaintView v, String folderToSave)
    {
    Bitmap b = Bitmap.createBitmap(v.getHeight(), v.getWidth(), Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(b);
    v.layout(0, 0, v.getLayoutParams().width, v.getLayoutParams().height);
    v.draw(c);
    OutputStream fOut = null;
    Time time = new Time();
    time.setToNow();

    try {
    File file = new File(folderToSave, Integer.toString(time.year) + Integer.toString(time.month) + Integer.toString(time.monthDay) + Integer.toString(time.hour) + Integer.toString(time.minute) + Integer.toString(time.second) +".jpg"); // создать уникальное имя для файла основываясь на дате сохранения
    fOut = new FileOutputStream(file);
    b.compress(Bitmap.CompressFormat.JPEG, 85, fOut); // сохранять картинку в jpeg-формате с 85% сжатия.
    fOut.flush();
    fOut.close();
    }
    catch (Exception e)
    {}

    }

    ОтветитьУдалить
  12. Довёл таки до кондиции рисовалку.
    При linew > 1 рисовать не реально. Нужно очевидно не прямоугольник а круг рисовать, а для этого наверное придется отдельный класс "кружочков" создавать.

    Автору большое спасибо!

    ОтветитьУдалить
  13. А можете показать весь код MainActivity?А то я немножко не понял

    ОтветитьУдалить
  14. Этож что за пример такой, автор видимо перепостил откуда-то, рисовать line и rect, накуй спаршивается, все инатми задается брееед, но все равно спасибо, у кого есть желание тот разберется, для тех кто хочет видеть MainActivity
    public class MainActivity extends Activity {

    ArrayList arrayPoint = new ArrayList();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Resources res = getResources();
    int h = res.getDisplayMetrics().heightPixels;
    int w = res.getDisplayMetrics().widthPixels;

    PaintView paintView = new PaintView(this, 3, 100, 2, w, h);
    setContentView(paintView);
    }

    public void addPoint(Point point) {
    arrayPoint.add(point);
    }

    public ArrayList getPoints() {
    return arrayPoint;
    }
    }

    ОтветитьУдалить
    Ответы
    1. После ArrayList надо поставить Point в знаках меньше и больше <>, при размещение комментария они съедаются

      Удалить
  15. Большое вам спасибо!!!

    ОтветитьУдалить