Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[부나] 뷰 챌린지 미션 2단계 제출합니다. #32

Merged
merged 25 commits into from
Oct 10, 2023

Conversation

tmdgh1592
Copy link
Member

@tmdgh1592 tmdgh1592 commented Oct 8, 2023

안녕하세요 해시! ☀️
만족한 기능 요구사항은 다음과 같습니다.

기능 요구 사항

그림판 앱에 기능을 추가한다.

  • 화면에 사각형, 원 모양을 그릴 수 있도록 구현한다.
  • 사용자가 자유롭게 크기나 위치를 조정할 수 있다.
  • 특정 영역의 요소를 지울 수 있는 지우개를 구현한다.
  • 브러쉬(펜, 사각형, 원, 지우개) 종류를 자유롭게 선택해서 그릴 수 있다.

선택 요구 사항

  • 전체 그림을 지우는 기능을 추가한다.
  • 취소(undo), 재실행(redo)을 구현한다.

또한, 도형, 지우개 등이 추가되면서 기존에 PathPainter 클래스를 그대로 사용하는 대신, 모두 Painter interface를 구현하도록 변경하였습니다.
리뷰 잘 부탁드립니다 😃

@tmdgh1592 tmdgh1592 requested a review from EmilyCh0 October 8, 2023 02:46
Copy link
Member

@EmilyCh0 EmilyCh0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 부나! 2단계 미션 고생하셨습니다~
궁금한 점 위주로 코멘트 남겼으니 확인 후 다시 리뷰 요청해 주세요!

fun Paint.softPainter(
paletteColor: PaletteColor = PaletteColor.RED,
paintStyle: Paint.Style = Paint.Style.STROKE,
porterDuffMode: PorterDuff.Mode = PorterDuff.Mode.SRC_OVER,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PorterDuff.Mode.SRC_OVER 는 어떤 옵션인가요??

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존에 그려져 있는 리소스 위에 작성하겠다는 의미입니다 : )

Comment on lines 44 to 46
fun onActionUp(x: Float, y: Float) {
currentPainter = currentPainter.extract()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인자가 사용되지 않아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

언젠가 onActionUp 되었을 때 좌표가 필요한 경우가 있을 것 같아서 지우지 않았는데,
생각해보니 필요한 시점에 추가하는 게 낫겠네요 👍

Comment on lines 10 to 11
private val path: Path = Path(),
val paint: Paint = Paint().softPainter(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path는 private으로 변경되었는데 paint는 public인 이유가 있나요??

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDE에서 따로 알려주지 않아서 몰랐네요!
private으로 변경하겠습니다 😃

Comment on lines 62 to 65
override fun extract(): Painter = copy(
path = Path(),
paint = Paint(paint),
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 코드는 어떤 기능을 하나요??

Copy link
Member Author

@tmdgh1592 tmdgh1592 Oct 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제 코드는 onActionDown되었을 때 리스트에 추가하고,
onActionUp 되었을 때 새로운 Painter로 변경해주는 방식으로 Undo 리스트를 관리하고 있습니다.

그래서 각 Painter별로 새로운 객체를 생성하는 기능이 필요해서 해당 코드를 구현하였습니다.

단순 data class이면 copy()를 사용하면 되는데, Painter라는 추상 클래스를 구현하다보니 외부에서는 copy()를 사용할 수가 없습니다.. 🥲
해시가 알고 있는 좋은 방법이 있을까요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아아 extract()로 리셋하지 않으면 색상이나 두께를 변경하지 않고 여러 개를 그렸을때 문제가 있겠네요..! 저도 비슷한 방식으로 copy하고 있어요. 다만 저는 아래 코드처럼 구현해서 색상이나 두께 변경 시에도 같은 메서드 사용하고 있습니다! 더 좋은 방법이 있는지는 아직 잘 모르겠네요..

override fun copy(color: Int?, width: Float?): Brush {
    val newPaint = Paint(paint).apply {
        this.color = color ?: paint.color
        this.strokeWidth = width ?: paint.strokeWidth
    }
    return LineBrush(Path(), newPaint)
}

Copy link
Member Author

@tmdgh1592 tmdgh1592 Oct 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 코드를 적용해보고자 하였는데 제 코드는 생성자에 필요한 인자가 각각 달라서 적용이 힘들겠네요ㅠㅠ!
올려주신 코드도 좋아 보이네요~

Comment on lines 43 to 45
override fun draw(canvas: Canvas) {
canvas.drawArc(rect, 0F, 360F, true, paint)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canvas.drawOval()과 다른가요 ?_?

Copy link
Member Author

@tmdgh1592 tmdgh1592 Oct 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다시 생각해보니 원의 360도 전체를 그릴거라면 drawOval() 을 사용해도 똑같네요 ^_^
인자도 훨씬 적어지구요!

Comment on lines 52 to 53
private val DEFAULT_PAINT: Paint
get() = Paint().softPainter(paintStyle = Paint.Style.FILL)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CirclePainter와 RectanglePainter도 softPainter 여야 하나요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! softPainter의 내부를 확인해보면 antiAlias가 true로 설정되어 매끄럽게 그려주고 있습니다.
물론 그 외에 필요하지 않은 속성들도 있기는 하지만 도형을 그릴 때 문제가 되지 않는 속성이라 그대로 유지하였습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

antiAlias가 도형에도 영향을 주는 옵션이군요! 새로 알아갑니다 👍

Comment on lines 16 to 32
fun changePainter(
paletteMode: PaletteMode,
paletteShape: PaletteShape = PaletteShape.values().first(),
): Painter {
return when (paletteMode) {
PaletteMode.BRUSH -> BrushPainter()
PaletteMode.SHAPE -> getShapePainter(paletteShape)
PaletteMode.ERASER -> PathEraserPainter()
}
}

private fun getShapePainter(
paletteShape: PaletteShape,
): Painter = when (paletteShape) {
PaletteShape.RECTANGLE -> RectanglePainter(paletteShape)
PaletteShape.CIRCLE -> CirclePainter(paletteShape)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PainterHistory 내부에서 currentPainter를 변경하는 함수로 만들어도 될 것 같은데 Painter 인터페이스 내부에 구현하신 이유가 궁금합니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 네이밍이 changePainter로 되어 있고 정적이지는 않지만, 기능만 보면 사실 정적 팩토리 함수와 동일합니다.
그래서 필요한 인자를 전달해주면 Painter 객체를 생성해주는 역할을 Painter 자체에 부여하였습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인터페이스에 구현 메서드를 둔 것이 조금 어색하다 생각했는데 추상 클래스로 변경됐네요! 팩토리 함수라면 하위클래스가 재정의 할 일도 없으니 companion object에 두는 것은 어떤가요??

Copy link
Member Author

@tmdgh1592 tmdgh1592 Oct 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상태를 가지고 있지 않기 때문에 interface로 하였다가, paint라는 상태를 가지면서 추상클래스로 변경하였습니다~

팩토리 함수라면 하위클래스가 재정의 할 일도 없으니 companion object에 두는 것은 어떤가요??

코멘트만 달아놓고 아직 수정하지 않았었네요!
반영하겠습니다 👍

// Painter.kt

companion object {
    fun changePainter(
        paletteMode: PaletteMode,
        painterState: PainterState,
    ) { ... }

Comment on lines 19 to 20
var selectedPaintThickness: Float = THICKNESS_SIZE_UNIT
var selectedPaletteColor: PaletteColor = PaletteColor.values().first()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private set으로 두면 좀 더 안전할 것 같네요!

import woowacourse.paint.view.palette.color.PaletteColor

fun Paint.softPainter(
paletteColor: PaletteColor = PaletteColor.RED,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

팔레트 색상이나 굵기가 유지되면 사용자 입장에서 더 자연스러울 것 같아요! 현재는 지우개나 브러시를 선택했을 때 슬라이더 위치와 실제 그려지는 두께가 다르고, 버튼을 누를 때마다 색상이 빨간색으로 초기화되어 불편합니다.
이건 요구사항은 아니니까 자유롭게 반영해주세요! (어디에 코멘트 다는게 좋을지 모르겠어서 아무데나 남깁니다..ㅋㅋ)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아무래도 Painter의 changePainter() 메서드에서 매번 새로운 Painter를 반환해주니 생기는 문제인 것 같습니다 🥲
현재 PainterHistory에서 두께, 도형, 색상에 대한 상태를 가지고 있도록 하였고, 브러시 변경시 Painter.changePainter()의 인자로 이전의 상태를 전달하도록 변경하였습니다.

@tmdgh1592
Copy link
Member Author

안녕하세요 해시!
코드 리뷰 반영을 완료하여 재리뷰 요청드립니다 😃
말씀해주신 버그를 수정하였고, 관심사 분리를 통해 코드 구조를 최적화하였습니다.
잘 부탁드립니다 🙌

Copy link
Member

@EmilyCh0 EmilyCh0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 반영 수고하셨습니다! ShapePainter와 PainterState 코드도 추가되었네요! 사소한 코멘트 추가로 남겼으니 확인하고 다시 리뷰 요청해주세요!

Comment on lines +56 to +64
fun undo() {
val redo = undoes.removeLastOrNull() ?: return
redoes.add(redo)
}

fun redo() {
val undo = redoes.removeLastOrNull() ?: return
undoes.add(undo)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 정책 차이라고 생각해서 꼭 반영해야하는 부분은 아니라고 생각하는데..
일반적으로 노트나 그림판 앱을 보면 undo를 실행하고 다른 그림을 추가하면 redoes가 clear() 됩니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다!
하지만 저는 개인적으로 이러한 undo/redo 방식에 불편함을 느끼고 있었기에,
UNDO를 했을 때 clear 하지 않고 기존 데이터를 유지하는 정책을 선택했습니다!

Copy link
Member

@EmilyCh0 EmilyCh0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

step3도 화이팅입니다 💪

@EmilyCh0 EmilyCh0 merged commit 283afc7 into woowacourse:tmdgh1592 Oct 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants