도전과제 23 : 페인트보드 앱의 설정 기능 만들기 (Do it Android 앱 프로그래밍) [JAVA]
반응형

 

도전과제 23

페인트보드 앱에 CAP를 설정할 수 있는 기능을 만들어 보세요. 손가락으로 터치하여 선을 그릴 때 선이 가질 수 있는 속성 중의 하나입니다.

  1. 페인트보드 앱은 위쪽에 버튼이 있고 아래쪽에 손가락으로 선을 그릴 수 있는 패널이 있습니다. 위쪽에 있는 버튼들의 아래쪽에 레이아웃을 추가하고 그 안에 CAP 스타일을 선택할 수 있는 라디오 버튼들을 배치합니다.
  2. CAP 스타일을 표시하는 라디오 버튼을 선택하면 선을 그리는 Paint 객체에 선택한 CAP 스타일이 설정되게 합니다.
  3. CAP 스타일을 변경한 후 손가락을 터치해서 선을 그리면 설정한 선의 속성으로 그려지도록 합니다.

참고할점
선의 속성은 여러 가지가 있는데 선의 속성을 모두 넣고 싶다면 별도의 설정 화면을 만들 수도 있습니다.

 

풀이

메인 레이아웃은 단순하게 색상/굵기 두 버튼과 CAP 스타일 세가지 BUTT, ROUND, SQUARE를 선택할 수 있는 라디오버튼들로 구성했다.

이제 그림을 그릴 페인트보드 객체를 정의해준다. 그래픽을 그리기 위해서는 View 클래스를 상속한 후 이 뷰에 직접 그래픽을 그릴 수 있다. 뷰를 상속한 후, 그래픽을 그리기 위한 속성이 담긴 페인트 객체를 세팅해준다. 문제에서 원하는 선의 색상, 굵기, CAP 모두 페인트 객체에서 설정할 수 있다.

 //페인트 객체에 세팅
    public void init(Context context){
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(15.0F);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(strokeCap);

        this.lastX = -1;
        this.lastY= -1;
    }

이제 onDraw() 메서드를 통해 뷰가 화면에 그려질 때 그래픽을 그리도록 해야하는데, 모든 변화사항을 바로바로 뷰에 나타내기 보다 비트맵을 사용하여 캔버스로 -> 비트맵에 미리미리 그림을 그려놓고 -> 이를 뷰에 그리는 형태로 해서 좀더 부드럽게 표현될 수 있게 한다.

    protected void onDraw(Canvas canvas){
        if(mBitmap != null){
            canvas.drawBitmap(mBitmap,0,0,null);
        }
    }

이제 직접적으로 선을 그리는 방법을 구현해야 하는데, 간단한 원리는 x,y좌표를 계속해서 계산하여 움직인 만큼 계속 검은색 사각형을 칠해나가는 것이다. 이를 위해 화면이 터치될 때 호출되는 onTouchEvent를 구현해야 한다. ACTION_UP (손을 뗌) ACTION_DOWN (누름), ACTION_MOVE(움직임)일때 모두 동작을 정의하여준다. 조금 더 매끄러운 선을 그리기 위해 Path 객체와 곡선에서 curve를 사용하여 선을 그려나가는 책의 예제를 참고했다.

 public boolean onTouchEvent(MotionEvent event){
        int action = event.getAction();
        Rect rect;
        switch (action){
            case MotionEvent.ACTION_UP : {
                changed = true;
                rect = touchUp(event,false);
                if(rect!= null){
                    invalidate(rect);
                }
                mpath.rewind();
                return true;
            }

            case MotionEvent.ACTION_DOWN :{
                rect = touchDown(event);
                if(rect!= null){
                    invalidate(rect);
                }
                return true;
            }
            case MotionEvent.ACTION_MOVE : {
                rect = touchMove(event);
                if(rect!= null){
                    invalidate(rect);
                }
                return true;
            }
        }
        return false;
    }

    private Rect touchDown(MotionEvent event){
        float x = event.getX();
        float y =  event.getY();

        lastY = y;
        lastX = x;

        Rect mInvalidaRect = new Rect();
        mpath.moveTo(x,y);

        final int border = mInvalidateExtraBorder;
        mInvalidaRect.set((int)x-border, (int)y-border, (int) x+border,(int) y+border);
        mCurveEndX = x;
        mCurveEndY = y;
        mCanvas.drawPath(mpath,mPaint);
        return mInvalidaRect;
    }

    private Rect touchMove(MotionEvent event){
        Rect rect = processMove(event);
        return rect;
    }
    private Rect touchUp(MotionEvent event, boolean cancel){
        Rect rect = processMove(event);
        return rect;
    }

    private Rect processMove(MotionEvent event){
        final float x = event.getX();
        final float y = event.getY();
        final float dx = Math.abs(x-lastX);
        final float dy = Math.abs(y-lastY);

        Rect mInvalidRect = new Rect();
        if(dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE){
            final int border = mInvalidateExtraBorder;
            mInvalidRect.set((int)mCurveEndX-border, (int)mCurveEndY - border, (int)mCurveEndX+border, (int)mCurveEndY+border);

            float cx = mCurveEndX = (x+lastX)/2;
            float cy = mCurveEndY = (y+lastY)/2;

            mpath.quadTo(lastX,lastY,cx,cy);
            mInvalidRect.union((int)lastX - border, (int)lastY - border, (int)lastX+border, (int)lastY+border);
            mInvalidRect.union((int)cx - border, (int)cy- border, (int)cx+border, (int)cy+border);
            lastY = y;
            lastX = x;
            mCanvas.drawPath(mpath,mPaint);
        }
        return mInvalidRect;
    }

책에서 주어진 문제인 CAP 바꾸기는 라디오 버튼, 버튼에 따라 setStrokeCap 함수를 통해 할 수 있다. (색과 굵기는 미리 정해준 배열 내에서 선택하게 했다)

int color[] = {Color.BLACK, Color.RED};
float border[] = {5f, 10f, 15f, 30f};
    
public void setStrokeCap(Paint.Cap strokeCap) {
  mPaint.setStrokeCap(strokeCap);
}

public void setColor(boolean bool){
	if(bool){
		mPaint.setColor(color[0]);
	}
	else{
		mPaint.setColor(color[1]);
	}
}

public void setBorder(int i){
	mPaint.setStrokeWidth(border[i]);
}

매인 액티비티에서는 Linearlayout 객체를 선언하여, 위와 같이 만들어낸 페인트보드 뷰를 크기를 주어 add하면 된다. 라디오버튼과 버튼을 통해 원하는 속성으로 변경할 수 있다.

public class MainActivity extends AppCompatActivity {
    BestPaintBoard paintBoard;
    LinearLayout layout;
    RadioButton  BUTT;
    RadioButton  ROUND;
    RadioButton SQUARE;
    boolean colorbool = true;
    int border = 0;

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

        layout = findViewById(R.id.layout);
        paintBoard = new BestPaintBoard(this);
        layout.addView(paintBoard);
        paintBoard.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));

        final RadioGroup rg = (RadioGroup)findViewById(R.id.radiogroup);
        BUTT = findViewById(R.id.radioButton);
        ROUND = findViewById(R.id.radioButton2);
        ROUND.setChecked(true);
        SQUARE = findViewById(R.id.radioButton3);

        rg.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(RadioGroup radioGroup, int i) {
                if(i == R.id.radioButton){
                    paintBoard.setStrokeCap(Paint.Cap.BUTT);
                    Toast.makeText(getApplicationContext(),"BUTT", Toast.LENGTH_LONG).show();
                }
                else if(i == R.id.radioButton2){
                    paintBoard.setStrokeCap(Paint.Cap.ROUND);
                    Toast.makeText(getApplicationContext(),"ROUND", Toast.LENGTH_LONG).show();
                }
                else if (i == R.id.radioButton3){
                    paintBoard.setStrokeCap(Paint.Cap.SQUARE);
                    Toast.makeText(getApplicationContext(),"SQUARE", Toast.LENGTH_LONG).show();
                }
            }
        });

        final Button button = findViewById(R.id.buttoncolor);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                colorbool = !colorbool;
                paintBoard.setColor(colorbool);
            }
        });

        Button button1 = findViewById(R.id.buttonborder);
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                paintBoard.setBorder(border);
                if(++border >3){
                    border = border -4;
                }
            }
        });


    }
}

 

결과

전체 소스코드 : https://github.com/howtolivelikehuman/DoitAndroid/tree/master/DoitMission_23/app/src/main

 

howtolivelikehuman/DoitAndroid

Do it Android programing (JAVA). Contribute to howtolivelikehuman/DoitAndroid development by creating an account on GitHub.

github.com

 

 

반응형