이번에는 Canvas와 Paint를 사용하는 간단한 실습을 합시다.
사용자는 메뉴에서 그릴 도형을 선택(선, 원)할 수 있고 펜의 두께(얇게, 두껍게)를 선택하거나 색상(빨강, 파랑, 녹색)을 선택할 수 있습니다. 그리고 선만 그리게 하거나 원만 그리거나 전체를 그리는 것을 선택할 수 있고 전체 도형을 지우거나 가장 최근에 그린 도형을 지울 수 있습니다.
먼저 여기에서 그릴 두 가지 도형을 정의합시다. 먼저 선과 원의 기반 형식인 도형 클래스를 정의합시다. 도형은 시작 좌표와 끝 좌료를 멤버 필드로 갖고 펜의 두께와 색상을 갖습니다. 파생 형식에서 각 멤버 필드의 값을 변경할 수 있게 설정자를 정의하고 접근 지정은 protected로 지정하세요. 끝 좌표는 화면 터치를 이동할 때 계속 변할 수 있어야 하므로 접근 지정을 public으로 지정하세요.
package com.example.ehclub.ex_drawpaint; import android.graphics.Color; import android.graphics.Point; /** * Created by ehclub on 2017-06-12. */ public class Diagram { private Point start = new Point(-1,-1); private Point end = new Point(-1,-1); private int width = 1; private int color = Color.RED; protected void setStart(int x, int y){ start = new Point(x,y); } protected void setStart(Point pt){ start = pt; } public void setEnd(int x,int y){ end = new Point(x,y); } protected void setEnd(Point pt){ end = pt; } protected void setWidth(int width){ this.width = width; } protected void setColor(int color){ this.color = color; } public Point getStart(){ return start; } public Point getEnd(){ return end; } public int getWidth(){ return width; } public int getColor(){ return color; } }
Diagram에서 파생한 Circle 클래스를 정의하세요. 생성자 외에 반지름을 구하는 메서드와 중심 좌표를 구하는 메서드를 제공합시다.
package com.example.ehclub.ex_drawpaint; import android.graphics.Point; /** * Created by ehclub on 2017-06-12. */ public class Circle extends Diagram { public Circle(int x,int y, int width, int color){ setStart(x,y); setEnd(x,y); setWidth(width); setColor(color); } public int getRadius(){ int sx = getStart().x; int sy = getStart().y; int ex = getEnd().x; int ey = getEnd().y; return (int)Math.sqrt(Math.pow(ex-sx,2)+Math.pow(ey-sy,2)); } public int getCenterX(){ return getStart().x; } public int getCenterY(){ return getStart().y; } }
Diagram에서 파생한 Line 클래스를 정의합시다. 여기에서는 생성자를 정의하는 것으로 충분합니다.
package com.example.ehclub.ex_drawpaint; import android.graphics.Point; /** * Created by ehclub on 2017-06-12. */ public class Line extends Diagram { public Line(int x,int y, int width, int color){ setStart(x,y); setEnd(x,y); setWidth(width); setColor(color); } }
이제 MainActivity 클래스를 구현합시다. 먼저 메뉴 ID로 사용할 상수를 정의합시다.
final static int LINE=1, CIRCLE=2, THIN=3,THICK=4, RED=5,BLUE=6,GREEN=7, DLINE=8, DCIRCLE=9, DALL=10, CLEARALL=11, CLEARLAST=12;
도형 종류, 두께, 선의 색상, 그리기 모드를 위한 멤버는 그리기 작업을 할 View에서 파생한 형식 개체에서 접근할 수 있게 정적 멤버로 선언하세요.
static int dflag = LINE; static int width = 1; static int color = Color.RED; static int dmode = DALL;
현재까지 그린 도형을 보관할 컬렉션과 현재 그리고 있는 도형 개체를 기억할 멤버를 추가하세요.
static ArrayList<Diagram> diagrams = new ArrayList<Diagram>(); static Diagram now=null;
그리기 작업을 할 클래스 이름을 MyGraphicView로 정할 것입니다. MyGraphicView 개체를 기억할 멤버 필드를 선언하세요.
MyGraphicView mv;
onCreate 메서드에서는 MyGraphicView 개체를 생성하고 setContentView 메서드에 해당 개체로 설정합니다. 앞에서도 다뤘듯이 Layout을 위해 정의한 activity_main.xml 파일은 필요하지 않습니다.
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mv = new MyGraphicView(this); setContentView(mv); setTitle("Tiny Paint2"); }
onCreateOptionMenu 메서드를 재정의하여 옵션 메뉴 항목을 추가하세요.
@Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); menu.add(0,LINE,0,"LINE"); menu.add(0,CIRCLE,0,"CIRCLE"); menu.add(0,THIN,0,"THIN"); menu.add(0,THICK,0,"THICK"); menu.add(0,RED,0,"RED"); menu.add(0,BLUE,0,"BLUE"); menu.add(0,GREEN,0,"GREEN"); menu.add(0,DLINE,0,"Draw Only Line"); menu.add(0,DCIRCLE,0,"Draw Only Circle"); menu.add(0,DALL,0,"Draw All"); menu.add(0,CLEARALL,0,"Clear All"); menu.add(0,CLEARLAST,0,"Clear Last"); return true; }
onOptionsItemSelected 메서드를 재정의하여 옵션 메뉴 항목을 선택할 때의 처리를 구현합니다. 메뉴 선택에 따라 도형이 바뀌거나, 폭, 색상, 그리기 모드를 변경하세요. 그리고 전체 지우기 혹은 가장 최근에 항목 지우기를 선택하였을 때의 처리도 해 주어야 합니다. 전체 지우기를 선택하였을 때는 도형을 보관하는 컬렉션 diagrams의 clear 메서드를 호출하세요. 가장 최근에 도형을 지우는 부분은 별도의 메서드(clearLast)를 정의하여 호출하기로 합시다. 특히 여기에서는 선택에 따라 화면에 다시 그려야 할 수도 있으므로 MyGraphicView 개체를 참조하는 변수 mv의 invalidate 메서드를 호출합니다.
@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case CIRCLE: dflag = CIRCLE; break; case LINE: dflag = LINE; break; case THIN: width=1; break; case THICK: width=5; break; case RED: color = Color.RED; break; case BLUE: color = Color.BLUE; break; case GREEN: color = Color.GREEN; break; case DLINE: dmode = DLINE; break; case DCIRCLE: dmode = DCIRCLE; break; case DALL: dmode = DALL; break; case CLEARALL: diagrams.clear(); break; case CLEARLAST: clearLast(); break; } mv.invalidate(); return super.onOptionsItemSelected(item); }
clearLast 메서드에서는 diagrams의 맨 마지막 요소를 제거합니다.
private void clearLast() { if(diagrams.size()>0) { diagrams.remove(diagrams.size() - 1); } }
이제 View를 기반으로 파생한 MyGraphicView 클래스를 정의합시다.
private static class MyGraphicView extends View { public MyGraphicView(Context context){ super(context); } }
onTouchEvent 메서드를 재정의하여 화면 터치에 관한 처리를 합니다. 만약 DOWN일 때는 새로운 도형을 그리기 시작하는 것이며 MOVE일 때는 도형의 끝 좌표를 변경하는 것이고 UP은 그리고 있는 도형을 완성하는 것입니다. 각 부분을 별도의 메서드로 정의하기로 합시다.
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: makeStartDiagram(event); break; case MotionEvent.ACTION_MOVE: changeEndPoint(event); break; case MotionEvent.ACTION_UP: makeEndDiagram(event); break; } return true; }
먼저 도형을 그리기 시작하는 makeStartDiagram 메서드를 정의합시다. 현재 이벤트가 발생한 좌표를 얻어온 다음에 현재 그리는 도형에 따라 Circle 혹은 Line 개체를 생성합니다.
private void makeStartDiagram(MotionEvent event) { int x = (int)event.getX(); int y = (int)event.getY(); if(dflag==CIRCLE){ now = new Circle(x,y,width,color); } else{ now = new Line(x,y,width,color); } }
터치 Move일 때 끝 좌표를 변경하는 changeEndPoint 메서드를 정의합시다. 이벤트가 발생한 좌표로 도형의 끝 좌표를 설정한 후에 무효화 영역을 프로그램 방식으로 발생합니다.
private void changeEndPoint(MotionEvent event) { int x = (int)event.getX(); int y = (int)event.getY(); now.setEnd(x,y); this.invalidate(); }
현재 그리고 있는 도형을 확정하는 makeEndDiagram 메서드를 정의합시다. 이벤트가 발생한 좌표로 도형의 끝 좌표를 설정하고 도형 개체를 보관하는 컬렉션의 add 메서드를 호출하여 현재 도형 개체를 추가합니다. 그리고 현재 그리는 도형 개체를 null로 설정하고 무효화 영역을 프로그램 방식으로 발생합니다.
private void makeEndDiagram(MotionEvent event) { int x = (int)event.getX(); int y = (int)event.getY(); now.setEnd(x,y); diagrams.add(now); now = null; this.invalidate(); }
onDraw 메서드를 재정의하세요. 도형 컬렉션을 순차적으로 방문하여 그리는 작업과 현재 도형을 그리는 작업이 필요합니다. 특정 도형을 그리는 메서드(drawDiagram)를 별도로 정의하여 호출하세요.
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = new Paint(); paint.setAntiAlias(true); paint.setStyle(Paint.Style.STROKE); int i = 0; int last = diagrams.size(); Diagram diagram; for(i=0;i<last;++i){ diagram = diagrams.get(i); drawDiagram(diagram,canvas,paint); } if(now != null){ drawDiagram(now,canvas,paint); } }
이제 특정 도형을 그리는 drawDiagram 메서드를 정의합시다. 먼저 도형 개체의 폭과 색상을 얻어와서 Paint 개체의 속성을 설정합니다. 그리고 도형이 원인지 선인지 확인하여 해당 형식 개체로 참조한 후에 Canvas 개체의 그리기 메서드를 호출합니다.
private void drawDiagram(Diagram diagram, Canvas canvas, Paint paint) { paint.setStrokeWidth(diagram.getWidth()); paint.setColor(diagram.getColor()); if(diagram instanceof Circle){ if((dmode == DALL)||(dmode==DCIRCLE)) { Circle circle = (Circle) diagram; canvas.drawCircle(circle.getCenterX(), circle.getCenterY(), circle.getRadius(), paint); } } if(diagram instanceof Line){ if((dmode == DALL)||(dmode==DLINE)) { Line line = (Line) diagram; canvas.drawLine(line.getStart().x, line.getStart().y, line.getEnd().x, line.getEnd().y, paint); } } }
다음은 mainActivity.java 파일의 소스 코드입니다.
package com.example.ehclub.ex_drawpaint; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import java.util.ArrayList; public class MainActivity extends AppCompatActivity { final static int LINE=1, CIRCLE=2, THIN=3,THICK=4, RED=5,BLUE=6,GREEN=7, DLINE=8, DCIRCLE=9, DALL=10, CLEARALL=11, CLEARLAST=12; static int dflag = LINE; static int width = 1; static int color = Color.RED; static int dmode = DALL; static ArrayList<Diagram> diagrams = new ArrayList<Diagram>(); static Diagram now=null; MyGraphicView mv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mv = new MyGraphicView(this); setContentView(mv); setTitle("Tiny Paint2"); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); menu.add(0,LINE,0,"LINE"); menu.add(0,CIRCLE,0,"CIRCLE"); menu.add(0,THIN,0,"THIN"); menu.add(0,THICK,0,"THICK"); menu.add(0,RED,0,"RED"); menu.add(0,BLUE,0,"BLUE"); menu.add(0,GREEN,0,"GREEN"); menu.add(0,DLINE,0,"Draw Only Line"); menu.add(0,DCIRCLE,0,"Draw Only Circle"); menu.add(0,DALL,0,"Draw All"); menu.add(0,CLEARALL,0,"Clear All"); menu.add(0,CLEARLAST,0,"Clear Last"); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case CIRCLE: dflag = CIRCLE; break; case LINE: dflag = LINE; break; case THIN: width=1; break; case THICK: width=5; break; case RED: color = Color.RED; break; case BLUE: color = Color.BLUE; break; case GREEN: color = Color.GREEN; break; case DLINE: dmode = DLINE; break; case DCIRCLE: dmode = DCIRCLE; break; case DALL: dmode = DALL; break; case CLEARALL: diagrams.clear(); break; case CLEARLAST: clearLast(); break; } mv.invalidate(); return super.onOptionsItemSelected(item); } private void clearLast() { if(diagrams.size()>0) { diagrams.remove(diagrams.size() - 1); } } private static class MyGraphicView extends View { public MyGraphicView(Context context){ super(context); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: makeStartDiagram(event); break; case MotionEvent.ACTION_MOVE: changeEndPoint(event); break; case MotionEvent.ACTION_UP: makeEndDiagram(event); break; } return true; } private void makeEndDiagram(MotionEvent event) { int x = (int)event.getX(); int y = (int)event.getY(); now.setEnd(x,y); diagrams.add(now); now = null; this.invalidate(); } private void changeEndPoint(MotionEvent event) { int x = (int)event.getX(); int y = (int)event.getY(); now.setEnd(x,y); this.invalidate(); } private void makeStartDiagram(MotionEvent event) { int x = (int)event.getX(); int y = (int)event.getY(); if(dflag==CIRCLE){ now = new Circle(x,y,width,color); } else{ now = new Line(x,y,width,color); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = new Paint(); paint.setAntiAlias(true); paint.setStyle(Paint.Style.STROKE); int i = 0; int last = diagrams.size(); Diagram diagram; for(i=0;i<last;++i){ diagram = diagrams.get(i); drawDiagram(diagram,canvas,paint); } if(now != null){ drawDiagram(now,canvas,paint); } } private void drawDiagram(Diagram diagram, Canvas canvas, Paint paint) { paint.setStrokeWidth(diagram.getWidth()); paint.setColor(diagram.getColor()); if(diagram instanceof Circle){ if((dmode == DALL)||(dmode==DCIRCLE)) { Circle circle = (Circle) diagram; canvas.drawCircle(circle.getCenterX(), circle.getCenterY(), circle.getRadius(), paint); } } if(diagram instanceof Line){ if((dmode == DALL)||(dmode==DLINE)) { Line line = (Line) diagram; canvas.drawLine(line.getStart().x, line.getStart().y, line.getEnd().x, line.getEnd().y, paint); } } } } }