ref : Udemy [Android 12 및 Kotlin 개발 완전 정복]
세로모드 고정하기
프로젝트를 생성하고나서 먼저
항상 세로 모드이도록 설정 해준다.
Manifest.xml 파일에서 screenOrientaion 을 "portrait" 으로 지정해준다.
...
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter>
...
Paint, Canvas, Bitmap

Canvas 는 도화지, Bitmap 을 종이의 재료(나무)라고 생각하면된다.
그래서 Canvas 를 생성할 때에는 Bitmap 을 넣어주어야 한다.
그리고 이 도화지에 그리는게 Paint 인데 이런식으로 쓴다.
Paint paint = new Paint();
init
class에 init 블럭을 넣으면 객체 생성시에 호출이 되어 실행된다.
우리 프로젝트에서는 긴 변수 설정 부분을 setUpDrawing() 이라는 함수로 만들어
init에서 이 함수를 실행시켜주도록 따로 뺐다.
// 변수들을 설정
init{
setUpDrawing()
}
private fun setUpDrawing(){
mDrawPaint = Paint()
mDrawPath = CustomPath(color, mBrushSize)
mDrawPaint!!.color = color
mDrawPaint!!.style = Paint.Style.STROKE
mDrawPaint!!.strokeJoin = Paint.Join.ROUND //브러쉬의 끝부분 모양
mDrawPaint!!.strokeCap = Paint.Cap.ROUND // Cap 은 선 끝의 위치
mCanvasPaint = Paint(Paint.DITHER_FLAG) // DITHER_FLAG 는 사용불가 색상을 실험할 때 렌더린을 제어한다.
//mBrushSize = 20.toFloat()
}
MotionEvent
//화면을 터치할 때 그려지므로
override fun onTouchEvent(event: MotionEvent?): Boolean {
val touchX = event?.x
val touchY = event?.y
when(event?.action){
//action done 손가락을 화면에 댔을 때
//action move 화면을 손가락으로 드래그 했을 때
//action up 손가락을 들었을 때
MotionEvent.ACTION_DOWN -> {
//화면을 눌렀을 때 실행할 코드
mDrawPath!!.color = color
mDrawPath!!.brushThickness = mBrushSize
mDrawPath!!.reset()
mDrawPath!!.moveTo(touchX!!, touchY!!)
}
MotionEvent.ACTION_MOVE ->{
if (touchX != null) {
if (touchY != null) {
mDrawPath!!.lineTo(touchX, touchY)
}
}
}
MotionEvent.ACTION_UP -> {
mPaths.add(mDrawPath!!)
mDrawPath = CustomPath(color, mBrushSize)
}
else -> return false
}
//뷰가 화면에 보일 때 전체 뷰를 무효화 한다. 그리고 onDraw 가 그 후에 실행됨
invalidate()
return true
}
MotionEvent 객체는 터치했을 때 발생한 움직임에 대한 정보를 가지고 있다.
ACTION_DOWN = 누른다
ACTION_MOVE = 움직인다(드래그)
ACTION_UP = 뗀다
invalidate()
View 클래스에 정의된 메서드.
이 메서드를 호출하면, 해당 뷰 화면이 무효(invalid)임을 안드로이드에게 알리게 되고,
안드로이드는 현재의 뷰 상태를 반영하여 새로 화면을 그려준다.
드로잉 앱 프로젝트에서는 MotionEvent 를 처리한다음에 invalidate() 메소드를 쓰는 부분이 나오는데...
아래는 직관적인 이해를 위한(?)
외부에서 가져온 예시
class CustomView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
// 터치 X좌표값을 저장할 변수
var coordsX: Float? = null
// 화면 그리기
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// 텍스트 사이즈 설정
val paint = Paint()
paint.textSize = 30f
// coordX 변수에 저장된 값을 텍스트로 그린다.
canvas?.drawText("$coordsX", 100f, 100f, paint)
}
// 터치 이벤트 처리
override fun onTouchEvent(event: MotionEvent?): Boolean {
// 터치 X좌표 취득
coordsX = event!!.x
// ####### View 화면 다시 그리기
invalidate()
return true
}
}
사용자의 이벤트가 변경될 때마다 View 화면을 다시그리도록 하는 모습.

internal
프로젝트를 코딩하다보면 internal inner class 를 사용하는 과정이 나온다.
...
// Drawing View 내부에서만 사용
// Path 타입. Path 클래스는 직선, 2차 곡선 및 3차 곡선으로 구성된 복합 기하학적 path를 압축함.
internal inner class CustomPath(var color : Int, var brushThickness : Float) : Path(){ //Drawing View 내부에서만 사용
}
...
internal 는 가시성 변경자이다.
자바와 코틀린의 가시성 변경자의 차이를 먼저 알아보자
자바는 public, protect, private 변경자가 있다.
코틀린에서 다른 점은 2가지가 있는데
1. 아무 변경자도 없는 경우 선언은 모두 public 이다
2. 자바의 기본 가시성인 패키지 전용은 코틀린에 없다.
코틀린은 패키지를 네임스페이스를 관리하기 위한 용도로만 사용한다.
그래서 패키지를 가시성 제어에 사용하지 않는다.
-> 패키지 전용 가시성에 대한 대안으로 코틀린에는 internal 이라는 새로운 가시성 변경자가 도입되었다.
internal 은 같은 모듈 내에서만 볼 수있고, 여기서 모듈이랑 한꺼번에 컴파일되는 코틀린 파일들을 의미한다.

ConstraintLayout
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
내부의 객체를 배치할 때 부모 또는 자식 객체들간의 상대적인 위치로 배치할 수 있다.
객체의 4개의 면을 다른 객체의 특정 면에 제약 조건을 설정하여 상대적인 위치를 지정해줄 수 있다.
아래 속성들을 이용하여 객체간의 Contraint(제약) 조건을 설정하여 상대적인 배치를 할 수 있다.
상대적인 배치

중앙 배치

Margin

체인 스타일

-120강 까지의 결과물
