캔버스 객체 클리핑

이 Codelab은 Kotlin 기반 Android 고급 교육 과정의 일부입니다. Codelab을 순서대로 진행하는 경우 학습 효과를 극대화할 수 있지만 순서를 바꿔 진행해도 괜찮습니다. 모든 교육 과정 Codelab은 Kotlin 기반 고급 Android Codelab 방문 페이지에 나열되어 있습니다.

소개

이 Codelab의 목적에 따라 클리핑은 선택적으로 화면에 그려지거나 그려지지 않는 이미지, 캔버스 또는 비트맵의 영역을 정의하는 방법입니다. 클리핑의 한 가지 목적은 오버드로우를 줄이는 것입니다. 오버드로란 최종 이미지를 표시하기 위해 화면의 픽셀이 두 번 이상 그려지는 경우를 말합니다. 오버드로를 줄이면 그리기 성능을 극대화하기 위해 디스플레이의 픽셀이나 영역이 그려지는 횟수가 최소화됩니다. 클리핑을 사용하여 사용자 인터페이스 디자인과 애니메이션에서 흥미로운 효과를 만들 수도 있습니다.

예를 들어 아래와 같이 겹치는 카드 스택을 그릴 때 아래에서 위로 각 카드를 완전히 그리는 대신 표시되는 부분만 그리는 것이 일반적으로 더 효율적입니다. 클리핑 작업에도 비용이 들고 전반적으로 Android 시스템에서 많은 그리기 최적화를 수행하기 때문에 '일반적으로'라고 표현했습니다.

카드의 표시되는 부분만 그리려면 각 카드에 클리핑 영역을 지정합니다. 예를 들어 아래 다이어그램에서 클리핑 사각형이 이미지에 적용되면 사각형 내부의 부분만 표시됩니다.

클리핑 영역은 일반적으로 직사각형이지만 도형이나 도형의 조합, 심지어 텍스트일 수도 있습니다. 클리핑 영역 내부의 영역을 포함할지 제외할지도 지정할 수 있습니다. 예를 들어 원형 클리핑 영역을 만들어 원 외부의 내용만 표시할 수 있습니다.

이 Codelab에서는 다양한 클리핑 방법을 실험해 봅니다.

기본 요건

다음을 잘 알고 있어야 합니다.

  • Activity로 앱을 만들고 Android 스튜디오를 사용하여 실행하는 방법
  • Canvas를 만들고 그리는 방법
  • 맞춤 View를 만들고 onDraw()onSizeChanged()를 재정의하는 방법

학습할 내용

  • 객체를 클리핑하여 Canvas에 그리는 방법
  • 캔버스의 그리기 상태를 저장하고 복원하는 방법
  • 캔버스와 텍스트에 변환을 적용하는 방법

실습할 내용

  • 클리핑의 다양한 방법과 이러한 방법이 도형의 가시성에 미치는 영향을 보여주는 클리핑된 도형을 화면에 그리는 앱을 만듭니다.
  • 또한 변환되고 기울어진 텍스트를 그립니다.

ClippingExample 앱은 도형을 사용하고 결합하여 캔버스의 어떤 부분이 뷰에 표시되는지 지정하는 방법을 보여줍니다. 최종 앱은 아래 스크린샷과 같이 표시됩니다.

이 앱은 처음부터 빌드할 것이므로 프로젝트를 설정하고, 크기와 문자열을 정의하고, 일부 변수를 선언해야 합니다.

1단계: ClippingExample 프로젝트 만들기

  1. Empty Activity 템플릿을 사용하여 ClippingExample라는 Kotlin 프로젝트를 만듭니다. 패키지 이름 접두사로 com.example.android을 사용합니다.
  2. MainActivity.kt를 엽니다.
  3. onCreate() 메서드에서 기본 콘텐츠 뷰를 바꾸고 콘텐츠 뷰를 ClippedView의 새 인스턴스로 설정합니다. 이 뷰는 다음에 만들 클리핑 예시의 맞춤 뷰가 됩니다.
setContentView(ClippedView(this))
  1. MainActivity.kt와 동일한 수준에서 View을 확장하는 ClippedView이라는 맞춤 뷰의 새 Kotlin 파일과 클래스를 만듭니다. 아래와 같은 서명을 지정합니다. 나머지 작업은 모두 이 ClippedView 안에 있습니다. @JvmOverloads 주석은 Kotlin 컴파일러에 기본 매개변수 값을 대체하는 이 함수의 오버로드를 생성하도록 지시합니다.
class ClippedView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}

2단계: 측정기준 및 문자열 리소스 추가

  1. res/values/dimens.xml의 새 리소스 파일에서 잘린 뷰에 사용할 크기를 정의합니다. 이러한 기본 측정기준은 하드코딩되어 있으며 매우 작은 화면에 맞게 크기가 조정됩니다.
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">90dp</dimen>
   <dimen name="clipRectBottom">90dp</dimen>
   <dimen name="clipRectTop">0dp</dimen>
   <dimen name="clipRectLeft">0dp</dimen>

   <dimen name="rectInset">8dp</dimen>
   <dimen name="smallRectOffset">40dp</dimen>

   <dimen name="circleRadius">30dp</dimen>
   <dimen name="textOffset">20dp</dimen>
   <dimen name="strokeWidth">4dp</dimen>

   <dimen name="textSize">18sp</dimen>
</resources>

더 큰 화면에서 앱이 보기 좋게 표시되고 세부정보를 더 쉽게 볼 수 있도록 더 큰 화면에만 적용되는 더 큰 값이 있는 dimens 파일을 만들면 됩니다.

  1. Android 스튜디오에서 values 폴더를 마우스 오른쪽 버튼으로 클릭하고 New > Values resource file을 선택합니다.
  2. New Resource File 대화상자에서 파일 이름을 dimens로 지정합니다. 사용 가능한 한정자에서 가장 작은 화면 너비를 선택하고 >> 버튼을 클릭하여 선택한 한정자에 추가합니다. 가장 작은 화면 너비 상자에 480을 입력하고 확인을 클릭합니다.

  1. 파일이 아래와 같이 값 폴더에 표시됩니다.

  1. 파일이 표시되지 않으면 앱의 프로젝트 파일 뷰로 전환합니다. 새 파일의 전체 경로는 ClippingExample/app/src/main/res/values-sw480dp/dimens.xml와 같습니다.

  1. values-sw480dp/dimens.xml 파일의 기본 콘텐츠를 아래의 크기로 바꿉니다.
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">120dp</dimen>
   <dimen name="clipRectBottom">120dp</dimen>

   <dimen name="rectInset">10dp</dimen>
   <dimen name="smallRectOffset">50dp</dimen>

   <dimen name="circleRadius">40dp</dimen>
   <dimen name="textOffset">25dp</dimen>
   <dimen name="strokeWidth">6dp</dimen>
</resources>
  1. strings.xml에 다음 문자열을 추가합니다. 이러한 속성은 캔버스에 텍스트를 표시하는 데 사용됩니다.
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>

3단계: Paint 및 Path 객체 만들기 및 초기화

  1. 프로젝트의 Android 뷰로 다시 전환합니다.
  2. ClippedView에서 그릴 Paint 변수를 정의합니다. 앤티앨리어싱을 사용 설정하고 아래와 같이 측정기준에 정의된 획 너비와 텍스트 크기를 사용합니다.
private val paint = Paint().apply {
   // Smooth out edges of what is drawn without affecting shape.
   isAntiAlias = true
   strokeWidth = resources.getDimension(R.dimen.strokeWidth)
   textSize = resources.getDimension(R.dimen.textSize)
}
  1. ClippedView에서 그려진 항목의 경로를 로컬로 저장할 Path을 만들고 초기화합니다. android.graphics.Path를 가져옵니다.
private val path = Path()

4단계: 도형 설정

이 앱에서는 다양한 방식으로 클리핑된 여러 행과 두 열의 모양을 표시합니다.

모두 다음을 충족해야 합니다.

  • 컨테이너 역할을 하는 큰 직사각형 (정사각형)
  • 큰 직사각형을 가로지르는 대각선
  • 짧은 텍스트 문자열

이 단계에서는 나중에 사용할 때 치수를 한 번만 가져오도록 리소스에서 이러한 도형의 치수를 설정합니다.

  1. ClippedView에서 path 아래에 전체 도형 집합 주위의 클리핑 사각형의 측정기준 변수를 추가합니다.
private val clipRectRight = resources.getDimension(R.dimen.clipRectRight)
private val clipRectBottom = resources.getDimension(R.dimen.clipRectBottom)
private val clipRectTop = resources.getDimension(R.dimen.clipRectTop)
private val clipRectLeft = resources.getDimension(R.dimen.clipRectLeft)
  1. 직사각형의 인셋과 작은 직사각형의 오프셋 변수 추가
private val rectInset = resources.getDimension(R.dimen.rectInset)
private val smallRectOffset = resources.getDimension(R.dimen.smallRectOffset)
  1. 원의 반지름에 대한 변수를 추가합니다. 직사각형 내부에 그려진 원의 반지름입니다.
private val circleRadius = resources.getDimension(R.dimen.circleRadius)
  1. 직사각형 내부에 그려지는 텍스트의 오프셋과 텍스트 크기를 추가합니다.
private val textOffset = resources.getDimension(R.dimen.textOffset)
private val textSize = resources.getDimension(R.dimen.textSize)

4단계: 행 및 열 위치 설정

이 앱의 도형은 위에 설정된 측정기준 값에 따라 2개 열과 4개 행으로 표시됩니다. 이 계산은 이 Codelab에 포함되지 않지만 이 단계에 제공된 코드를 복사할 때 확인해 보세요.

  1. 두 열의 좌표를 설정합니다.
private val columnOne = rectInset
private val columnTwo = columnOne + rectInset + clipRectRight
  1. 변환된 텍스트의 마지막 행을 포함하여 각 행의 좌표를 추가합니다.
private val rowOne = rectInset
private val rowTwo = rowOne + rectInset + clipRectBottom
private val rowThree = rowTwo + rectInset + clipRectBottom
private val rowFour = rowThree + rectInset + clipRectBottom
private val textRow = rowFour + (1.5f * clipRectBottom)
  1. 앱을 실행합니다. 앱 이름 아래에 빈 흰색 화면이 표시됩니다.

onDraw()에서는 아래 앱 스크린샷에 표시된 대로 7개의 잘린 사각형을 그리는 메서드를 호출합니다. 직사각형은 모두 동일한 방식으로 그려집니다. 유일한 차이점은 정의된 클리핑 영역과 화면에서의 위치입니다.

사각형을 그리는 데 사용되는 알고리즘은 아래 다이어그램과 설명에 표시된 대로 작동합니다. 요약하자면 Canvas의 원점을 이동하여 일련의 직사각형을 그립니다. 개념적으로 이는 다음 단계로 구성됩니다.

(1) 먼저 Canvas를 사각형을 그릴 위치로 변환합니다. 즉, 다음 사각형과 다른 모든 도형을 그려야 하는 위치를 계산하는 대신 Canvas 원점, 즉 좌표계를 이동합니다.

(2) 그런 다음 캔버스의 새 원점에 직사각형을 그립니다. 즉, 변환된 좌표계에서 동일한 위치에 도형을 그립니다. 훨씬 간단하고 약간 더 효율적입니다.

(3) 마지막으로 Canvas을 원래 Origin로 복원합니다.

다음은 구현할 알고리즘입니다.

  1. onDraw()에서 함수를 호출하여 Canvas을 회색 배경 색상으로 채우고 원래 도형을 그립니다.
  2. 잘린 각 사각형과 그릴 텍스트에 대해 함수를 호출합니다.

각 사각형 또는 텍스트에 대해 다음을 수행합니다.

  1. 초기 상태로 재설정할 수 있도록 Canvas의 현재 상태를 저장합니다.
  2. 캔버스의 Origin를 그릴 위치로 변환합니다.
  3. 클리핑 모양과 경로를 적용합니다.
  4. 직사각형 또는 텍스트를 그립니다.
  5. Canvas의 상태를 복원합니다.

단계: onDraw() 재정의

  1. 아래 코드와 같이 onDraw()를 재정의합니다. 나중에 구현할 각 도형에 대해 함수를 호출합니다.
 override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawBackAndUnclippedRectangle(canvas)
        drawDifferenceClippingExample(canvas)
        drawCircularClippingExample(canvas)
        drawIntersectionClippingExample(canvas)
        drawCombinedClippingExample(canvas)
        drawRoundedRectangleClippingExample(canvas)
        drawOutsideClippingExample(canvas)
        drawSkewedTextExample(canvas)
        drawTranslatedTextExample(canvas)
        // drawQuickRejectExample(canvas)
    }
  1. 코드가 계속 컴파일되도록 각 그리기 함수의 스텁을 만듭니다. 아래 코드를 복사하면 됩니다.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
}
private fun drawDifferenceClippingExample(canvas: Canvas){
}
private fun drawCircularClippingExample(canvas: Canvas){
}
private fun drawIntersectionClippingExample(canvas: Canvas){
}
private fun drawCombinedClippingExample(canvas: Canvas){
}
private fun drawRoundedRectangleClippingExample(canvas: Canvas){
}
private fun drawOutsideClippingExample(canvas: Canvas){
}
private fun drawTranslatedTextExample(canvas: Canvas){
}
private fun drawSkewedTextExample(canvas: Canvas){
}
private fun drawQuickRejectExample(canvas: Canvas){
}

앱은 동일한 직사각형과 도형을 7번 그립니다. 처음에는 클리핑 없이, 다음 6번은 다양한 클리핑 경로를 적용하여 그립니다. drawClippedRectangle() 메서드는 아래와 같이 하나의 직사각형을 그리는 코드를 인수화합니다.

1단계: drawClippedRectangle() 메서드 만들기

  1. Canvas 유형의 인수 canvas를 사용하는 drawClippedRectangle() 메서드를 만듭니다.
private fun drawClippedRectangle(canvas: Canvas) {
}
  1. drawClippedRectangle() 메서드 내에서 전체 도형의 클리핑 직사각형 경계를 설정합니다. 사각형만 그리도록 제한하는 클리핑 직사각형을 적용합니다.
canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
)

Canvas.clipRect(...) 메서드는 향후 그리기 작업이 쓸 수 있는 화면 영역을 줄입니다. 클리핑 경계를 현재 클리핑 사각형과 clipRect()에 전달된 사각형의 공간 교차점으로 설정합니다. 영역에 대해 다양한 형식을 허용하고 클리핑 사각형에 대해 다양한 작업을 허용하는 clipRect() 메서드의 변형이 많이 있습니다.

  1. canvas을 흰색으로 채워 줘. 예. 전체 캔버스. 사각형을 그리는 것이 아니라 클리핑하기 때문입니다. 클리핑 사각형 때문에 클리핑 사각형으로 정의된 영역만 채워져 흰색 사각형이 만들어집니다. 나머지 표면은 회색으로 유지됩니다.
canvas.drawColor(Color.WHITE)
  1. 색상을 빨간색으로 변경하고 클리핑 직사각형 안에 대각선을 그립니다.
paint.color = Color.RED
canvas.drawLine(
   clipRectLeft,clipRectTop,
   clipRectRight,clipRectBottom,paint
)
  1. 색상을 녹색으로 설정하고 클리핑 직사각형 안에 원을 그립니다.
paint.color = Color.GREEN
canvas.drawCircle(
   circleRadius,clipRectBottom - circleRadius,
   circleRadius,paint
)
  1. 색상을 파란색으로 설정하고 클리핑 직사각형의 오른쪽 가장자리에 정렬된 텍스트를 그립니다. canvas.drawText()를 사용하여 텍스트를 그립니다.
paint.color = Color.BLUE
// Align the RIGHT side of the text with the origin.
paint.textSize = textSize
paint.textAlign = Paint.Align.RIGHT
canvas.drawText(
   context.getString(R.string.clipping),
   clipRectRight,textOffset,paint
)

2단계: drawBackAndUnclippedRectangle() 메서드 구현

  1. drawClippedRectangle() 메서드가 작동하는 것을 확인하려면 아래와 같이 drawBackAndUnclippedRectangle() 메서드를 구현하여 첫 번째 클리핑되지 않은 사각형을 그립니다. canvas를 저장하고 첫 번째 행과 열 위치로 변환하고 drawClippedRectangle()를 호출하여 그린 다음 canvas를 이전 상태로 복원합니다.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
   canvas.drawColor(Color.GRAY)
   canvas.save()
   canvas.translate(columnOne,rowOne)
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. 앱을 실행합니다. 회색 배경에 원, 빨간색 선, 텍스트가 있는 첫 번째 흰색 사각형이 표시됩니다.

다음 클리핑 예시 방법에서는 다양한 클리핑 영역 조합을 적용하여 그래픽 효과를 얻고 클리핑 영역을 결합하여 필요한 모양을 만드는 방법을 알아봅니다.

이러한 각 메서드는 동일한 패턴을 따릅니다.

  1. 캔버스의 현재 상태를 저장합니다. canvas.save()

활동 컨텍스트는 그리기 상태 스택을 유지합니다. 그리기 상태는 현재 변환 매트릭스와 현재 클리핑 영역으로 구성됩니다. 현재 상태를 저장하고, 그림 상태를 변경하는 작업 (예: 캔버스 변환 또는 회전)을 실행한 다음 저장된 그림 상태를 복원할 수 있습니다. (참고: 이는 Git의 'stash' 명령어와 같습니다.)

그림에 변환이 포함된 경우 변환을 반전하여 변환을 연결하고 실행취소하면 오류가 발생하기 쉽습니다. 예를 들어 변환, 늘리기, 회전을 차례로 적용하면 복잡해집니다. 대신 캔버스의 상태를 저장하고, 변환을 적용하고, 그리고 이전 상태를 복원합니다.

예를 들어 클리핑 영역을 정의하고 해당 상태를 저장할 수 있습니다. 그런 다음 캔버스를 변환하고 클리핑 영역을 추가하고 회전합니다. 그림을 그린 후 원래 클리핑 상태를 복원하고 다이어그램에 표시된 대로 다른 변환 및 기울이기 변환을 진행할 수 있습니다.

  1. 캔버스의 원점을 행/열 좌표로 변환합니다. canvas.translate()

캔버스의 원점을 이동하고 새 좌표계에서 동일한 항목을 그리는 것이 모든 요소를 이동하여 그리는 것보다 훨씬 간단합니다. (팁: 요소를 회전하는 데 동일한 기법을 사용할 수 있습니다.)

  1. path에 변환을 적용합니다(있는 경우).
  2. 클리핑 적용: canvas.clipPath(path)
  3. 도형을 그립니다. drawClippedRectangle() or drawText()
  4. 이전 캔버스 상태 복원: canvas.restore()

1단계: drawDifferenceClippingExample(canvas) 구현

두 클리핑 직사각형 간의 차이를 사용하여 액자 효과를 만드는 두 번째 직사각형을 그리는 코드를 추가합니다.

다음을 실행하는 아래 코드를 사용하세요.

  1. 캔버스를 저장합니다.
  2. 캔버스의 원점을 첫 번째 행, 두 번째 열, 첫 번째 사각형의 오른쪽으로 이동합니다.
  3. 클리핑 직사각형 두 개를 적용합니다. DIFFERENCE 연산자는 첫 번째 사각형에서 두 번째 사각형을 뺍니다.
  1. drawClippedRectangle() 메서드를 호출하여 수정된 캔버스를 그립니다.
  2. 캔버스 상태를 복원합니다.
private fun drawDifferenceClippingExample(canvas: Canvas) {
   canvas.save()
   // Move the origin to the right for the next rectangle.
   canvas.translate(columnTwo,rowOne)
   // Use the subtraction of two clipping rectangles to create a frame.
   canvas.clipRect(
       2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .DIFFERENCE) was deprecated in API level 26. The recommended
   // alternative method is clipOutRect(float, float, float, float),
   // which is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O){
       canvas.clipRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset,
            Region.Op.DIFFERENCE
       )
   } else {
       canvas.clipOutRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. 앱을 실행하면 다음과 같이 표시됩니다.

2단계: drawCircularClippingExample(canvas) 구현

다음으로 원형 경로에서 생성된 원형 클리핑 영역을 사용하는 사각형을 그리는 코드를 추가합니다. 이렇게 하면 원이 삭제 (그려지지 않음)되어 대신 회색 배경이 표시됩니다.

private fun drawCircularClippingExample(canvas: Canvas) {

   canvas.save()
   canvas.translate(columnOne, rowTwo)
   // Clears any lines and curves from the path but unlike reset(),
   // keeps the internal data structure for faster reuse.
   path.rewind()
   path.addCircle(
       circleRadius,clipRectBottom - circleRadius,
       circleRadius,Path.Direction.CCW
   )
   // The method clipPath(path, Region.Op.DIFFERENCE) was deprecated in
   // API level 26. The recommended alternative method is
   // clipOutPath(Path), which is currently available in
   // API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipPath(path, Region.Op.DIFFERENCE)
   } else {
       canvas.clipOutPath(path)
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

3단계: drawIntersectionClippingExample(canvas) 구현

다음으로 두 클리핑 직사각형의 교차점을 두 번째 행과 열에 그리는 코드를 추가합니다.

화면 해상도에 따라 이 영역의 모양이 달라질 수 있습니다. smallRectOffset 측정기준을 사용하여 표시 영역의 크기를 변경해 보세요. smallRectOffset가 작을수록 화면의 영역이 커집니다.

private fun drawIntersectionClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowTwo)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight - smallRectOffset,
       clipRectBottom - smallRectOffset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .INTERSECT) was deprecated in API level 26. The recommended
   // alternative method is clipRect(float, float, float, float), which
   // is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom,
           Region.Op.INTERSECT
       )
   } else {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

4단계: drawCombinedClippingExample(canvas) 구현

다음으로 원과 사각형 모양을 결합하고 클리핑 영역을 정의하는 경로를 그립니다.

private fun drawCombinedClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne, rowThree)
   path.rewind()
   path.addCircle(
       clipRectLeft + rectInset + circleRadius,
       clipRectTop + circleRadius + rectInset,
       circleRadius,Path.Direction.CCW
   )
   path.addRect(
       clipRectRight / 2 - circleRadius,
       clipRectTop + circleRadius + rectInset,
       clipRectRight / 2 + circleRadius,
       clipRectBottom - rectInset,Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

5단계: drawRoundedRectangleClippingExample(canvas) 구현

다음으로 일반적으로 사용되는 클리핑 모양인 둥근 직사각형을 추가합니다.

  1. 최상위 수준에서 직사각형 변수를 만들고 초기화합니다. RectF은 부동 소수점으로 된 직사각형 좌표를 보유하는 클래스입니다.
private var rectF = RectF(
   rectInset,
   rectInset,
   clipRectRight - rectInset,
   clipRectBottom - rectInset
)
  1. drawRoundedRectangleClippingExample() 함수를 구현합니다. addRoundRect() 함수는 직사각형, 모서리 반지름의 x 및 y 값, 둥근 직사각형의 윤곽선을 감는 방향을 사용합니다. Path.Direction은 닫힌 도형 (예: 직사각형, 타원)이 경로에 추가될 때 방향을 지정합니다. CCW은 시계 반대 방향을 의미합니다.
private fun drawRoundedRectangleClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowThree)
   path.rewind()
   path.addRoundRect(
       rectF,clipRectRight / 4,
       clipRectRight / 4, Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

6단계: drawOutsideClippingExample(canvas) 구현

클리핑 직사각형의 인셋을 두 배로 늘려 직사각형 주변의 외부를 클립합니다.

private fun drawOutsideClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne,rowFour)
   canvas.clipRect(2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset)
   drawClippedRectangle(canvas)
   canvas.restore()
}

7단계: drawTranslatedTextExample(canvas) 구현

텍스트 그리기는 다른 도형과 크게 다르지 않으며 텍스트에 변환을 적용할 수 있습니다. 예를 들어 캔버스를 번역하고 텍스트를 그려 텍스트를 번역할 수 있습니다.

  1. 아래 함수를 구현합니다.
private fun drawTranslatedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.GREEN
   // Align the RIGHT side of the text with the origin.
   paint.textAlign = Paint.Align.LEFT
   // Apply transformation to canvas.
   canvas.translate(columnTwo,textRow)
   // Draw text.
   canvas.drawText(context.getString(R.string.translated),
       clipRectLeft,clipRectTop,paint)
   canvas.restore()
}
  1. 앱을 실행하여 번역된 텍스트를 확인합니다.

8단계: drawSkewedTextExample(canvas) 구현

텍스트를 기울일 수도 있습니다. 즉, 다양한 방식으로 왜곡합니다.

  1. ClippedView에서 아래 함수를 만듭니다.
private fun drawSkewedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.YELLOW
   paint.textAlign = Paint.Align.RIGHT
   // Position text.
   canvas.translate(columnTwo, textRow)
   // Apply skew transformation.
   canvas.skew(0.2f, 0.3f)
   canvas.drawText(context.getString(R.string.skewed),
       clipRectLeft, clipRectTop, paint)
   canvas.restore()
}
  1. 앱을 실행하여 번역된 텍스트 앞에 기울어진 텍스트가 그려지는지 확인합니다.

quickReject() Canvas 메서드를 사용하면 모든 변환이 적용된 후 지정된 직사각형이나 경로가 현재 표시되는 영역 완전히 외부에 있는지 확인할 수 있습니다.

quickReject() 메서드는 더 복잡한 그림을 구성하고 최대한 빨리 구성해야 할 때 매우 유용합니다. quickReject()를 사용하면 전혀 그리지 않아도 되는 객체를 효율적으로 결정할 수 있으며 교차 논리를 직접 작성할 필요가 없습니다.

  • quickReject() 메서드는 직사각형이나 경로가 화면에 전혀 표시되지 않는 경우 true을 반환합니다. 부분적으로 중복되는 경우에도 직접 확인해야 합니다.
  • EdgeType는 가장 가까운 픽셀로 반올림하는 경우 AA (앤티앨리어싱: 앤티앨리어싱될 수 있으므로 가장자리를 반올림하여 처리) 또는 BW (흑백: 가장자리를 가장 가까운 픽셀 경계로 반올림하여 처리)입니다.

quickReject()에는 여러 버전이 있으며 문서에서도 확인할 수 있습니다.

boolean

quickReject(float left, float top, float right, float bottom, Canvas.EdgeType type)

boolean

quickReject(RectF rect, Canvas.EdgeType type)

boolean

quickReject(Path path, Canvas.EdgeType type)

이 연습에서는 이전과 마찬가지로 텍스트 아래의 새 행에 clipRect 안에 그림을 그립니다.

  • 먼저 clipRect와 겹치는 사각형 inClipRectanglequickReject()를 호출합니다. 따라서 quickReject()는 false를 반환하고 clipRectBLACK로 채워지며 inClipRectangle 직사각형이 그려집니다.

  • 그런 다음 코드를 변경하고 notInClipRectangle을 사용하여 quickReject()을 호출합니다. 이제 quickReject()가 true를 반환하고 clipRectWHITE로 채워지며 notInClipRectangle가 그려지지 않습니다.

복잡한 그림이 있는 경우 클리핑 영역 밖에 완전히 있는 도형과 클리핑 영역 안에 부분적으로 또는 완전히 있는 도형을 빠르게 알 수 있으므로 추가 계산 및 그리기를 해야 할 수 있습니다.

단계: quickReject() 실험

  1. 최상위 수준에서 추가 행의 y 좌표 변수를 만듭니다.
   private val rejectRow = rowFour + rectInset + 2*clipRectBottom
  1. 다음 drawQuickRejectExample() 함수를 ClippedView에 추가합니다. quickReject() 사용에 필요한 모든 정보가 포함되어 있으므로 코드를 읽어보세요.
private fun drawQuickRejectExample(canvas: Canvas) {
   val inClipRectangle = RectF(clipRectRight / 2,
       clipRectBottom / 2,
       clipRectRight * 2,
       clipRectBottom * 2)

   val notInClipRectangle = RectF(RectF(clipRectRight+1,
       clipRectBottom+1,
       clipRectRight * 2,
       clipRectBottom * 2))

   canvas.save()
   canvas.translate(columnOne, rejectRow)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
   )
   if (canvas.quickReject(
           inClipRectangle, Canvas.EdgeType.AA)) {
       canvas.drawColor(Color.WHITE)
   }
   else {
       canvas.drawColor(Color.BLACK)
       canvas.drawRect(inClipRectangle, paint
       )
   }
       canvas.restore()
}
  1. onDraw()에서 drawQuickRejectExample() 호출의 주석 처리를 삭제합니다.
  2. 앱을 실행하면 채워진 클리핑 영역인 검은색 직사각형과 inClipRectangle의 일부가 표시됩니다. 두 직사각형이 겹치므로 quickReject()false를 반환하고 inClipRectangle가 그려집니다.

  1. drawQuickRejectExample()에서 notInClipRectangle.에 대해 quickReject()을 실행하도록 코드를 변경합니다. 이제 quickReject()true를 반환하고 클리핑 영역이 흰색으로 채워집니다.

완료된 Codelab의 코드를 다운로드합니다.

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-clipping


또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.

ZIP 파일 다운로드

  • 활동의 ContextCanvas의 변환 및 클리핑 영역을 보존하는 상태를 유지합니다.
  • canvas.save()canvas.restore()를 사용하여 캔버스를 그리고 원래 상태로 돌아갑니다.
  • 캔버스에 여러 도형을 그리려면 위치를 계산하거나 그리기 표면의 원점을 이동 (변환)하면 됩니다. 후자를 사용하면 반복되는 그리기 시퀀스를 위한 유틸리티 메서드를 더 쉽게 만들 수 있습니다.
  • 클리핑 영역은 모든 모양, 모양 조합 또는 경로일 수 있습니다.
  • 클리핑 영역을 추가, 빼기, 교차하여 필요한 영역을 정확하게 얻을 수 있습니다.
  • 캔버스를 변환하여 텍스트에 변환을 적용할 수 있습니다.
  • quickReject() Canvas 메서드를 사용하면 지정된 직사각형이나 경로가 현재 표시되는 영역 완전히 외부에 있는지 확인할 수 있습니다.

Udacity 과정:

Android 개발자 문서:

Android 프레임워크가 화면에 그리는 방식에 관한 자세한 설명은 그래픽 아키텍처 시리즈의 도움말을 참고하세요.

이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 진행하는 학생에게 출제할 수 있는 과제가 나열되어 있습니다. 다음 작업은 강사가 결정합니다.

  • 필요한 경우 과제를 할당합니다.
  • 과제 제출 방법을 학생에게 알립니다.
  • 과제를 채점합니다.

강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 적절하다고 생각되는 다른 과제를 출제해도 됩니다.

이 Codelab을 직접 진행하는 경우 이러한 과제를 자유롭게 사용하여 배운 내용을 테스트해 보세요.

질문에 답하세요

질문 1

효율적으로 도형이 그려지지 않도록 제외하려면 어떤 메서드를 호출해야 하나요?

excludeFromDrawing()

quickReject()

onDraw()

clipRect()

질문 2

Canvas.save()Canvas.restore()은 어떤 정보를 저장하고 복원하나요?

▢ 색상, 선 너비 등

▢ 현재 변환만

▢ 현재 변환 및 클리핑 영역

▢ 현재 클리핑 영역만

질문 3

Paint.Align는 다음을 지정합니다.

▢ 다음 그림 도형을 정렬하는 방법

▢ 텍스트가 그려지는 원점의 측면

▢ 클리핑 영역에서 정렬되는 위치

▢ 텍스트의 어느 쪽을 원점에 맞출지

이 과정의 다른 Codelab 링크는 Kotlin 기반 Android 고급 Codelab 방문 페이지를 참고하세요.