맞춤 그리기

맞춤 뷰에서 가장 중요한 부분은 디자인입니다. 맞춤 그리기는 애플리케이션의 필요에 따라 쉽거나 복잡할 수 있습니다. 이 과정에서는 가장 일반적인 작업 몇 가지에 관해 설명합니다.

이 과정 외에도 캔버스 및 드로어블에서 이와 관련된 추가 정보를 얻으실 수 있습니다.

onDraw() 재정의

맞춤 뷰를 그릴 때 가장 중요한 단계는 onDraw() 메서드를 재정의하는 것입니다. onDraw()의 매개변수는 뷰에서 스스로를 그리는 데 사용할 수 있는 Canvas 개체입니다. Canvas 클래스에서는 텍스트, 선, 비트맵, 기타 여러 그래픽 프리미티브를 그리기 위한 메서드를 정의합니다. onDraw()에서 이 메서드를 사용하여 맞춤 사용자 인터페이스(UI)를 만들 수 있습니다.

그러나 그리기 메서드를 호출하려면 먼저 Paint 개체를 생성해야 합니다. 다음 섹션에서는 Paint에 관해 더 자세히 설명합니다.

그리기 개체 만들기

android.graphics 프레임워크에서는 그리기를 다음 두 영역으로 나눕니다.

  • 무엇을 그릴 것인가(Canvas에서 처리)
  • 어떻게 그릴 것인가(Paint에서 처리)

예를 들어 Canvas에서는 선을 그릴 수 있는 메서드를 제공하고 Paint에서는 이 선의 색을 정의할 수 있는 메서드를 제공합니다. Canvas에는 직사각형을 그릴 수 있는 메서드가 있는 반면, Paint에서는 이 직사각형을 색상으로 채울지, 아니면 빈 상태로 둘지 정의합니다. 간단히 말하자면 Canvas에서는 화면에서 그릴 수 있는 도형을 정의하는 반면, Paint에서는 개발자가 그리는 각 도형의 색상, 스타일, 글꼴 등을 정의합니다.

따라서 무언가를 그리려면 하나 이상의 Paint 개체를 만들어야 합니다. PieChart 예시에서는 init이라는 메서드에서 이 개체를 만듭니다. 이 메서드는 자바의 생성자에서 호출하지만 Google에서는 다음과 같이 Kotlin에서 인라인으로 초기화할 수 있습니다.

Kotlin

    private val textPaint = Paint(ANTI_ALIAS_FLAG).apply {
        color = textColor
        if (textHeight == 0f) {
            textHeight = textSize
        } else {
            textSize = textHeight
        }
    }

    private val piePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.FILL
        textSize = textHeight
    }

    private val shadowPaint = Paint(0).apply {
        color = 0x101010
        maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL)
    }
    

자바

    private void init() {
       textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
       textPaint.setColor(textColor);
       if (textHeight == 0) {
           textHeight = textPaint.getTextSize();
       } else {
           textPaint.setTextSize(textHeight);
       }

       piePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
       piePaint.setStyle(Paint.Style.FILL);
       piePaint.setTextSize(textHeight);

       shadowPaint = new Paint(0);
       shadowPaint.setColor(0xff101010);
       shadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));

       ...
    

개체를 미리 만드는 것은 중요한 최적화 작업입니다. 뷰를 다시 그리는 경우는 매우 자주 발생하며 다수의 그리기 개체는 많은 비용을 들여 초기화해야 합니다. onDraw() 메서드 내에서 그리기 개체를 만들면 성능이 크게 저하되고 UI가 느리게 표시될 수 있습니다.

레이아웃 이벤트 처리

맞춤 뷰를 올바르게 그리려면 크기를 알아야 합니다. 복잡한 맞춤 뷰에서는 화면 내 영역의 크기와 모양에 따라 여러 가지 레이아웃 계산을 수행해야 하는 경우가 많습니다. 뷰가 화면에서 차지하는 크기를 추측해서는 안 됩니다. 하나의 앱에서만 뷰를 사용한다 해도 이 앱은 세로 모드와 가로 모드에서 여러 화면 크기, 여러 화면 밀도, 다양한 가로세로 비율을 처리해야 합니다.

View에는 측정치를 처리할 수 있는 메서드가 많이 있지만 대부분은 재정의할 필요가 없습니다. 뷰의 크기를 특별히 제어할 필요가 없는 경우 onSizeChanged()라는 메서드 하나만 재정의하면 됩니다.

뷰에 크기가 처음 할당되고 어떤 이유로든 뷰의 크기가 변경되면 onSizeChanged()가 호출됩니다. 그릴 때마다 다시 계산하는 대신 onSizeChanged()에서 뷰의 위치, 크기, 그리고 뷰의 크기와 관련된 기타 값을 계산합니다. PieChart 예시에서 onSizeChanged()PieChart 뷰가 원형 차트의 경계 직사각형, 텍스트 라벨의 상대적 위치, 기타 시각적 요소를 계산하는 곳입니다.

뷰에 크기가 할당되면 레이아웃 관리자는 크기에 모든 뷰의 패딩이 포함되어 있다고 가정합니다. 뷰의 크기를 계산할 때 패딩 값을 처리해야 합니다. 다음은 이렇게 하는 방법을 보여주는 PieChart.onSizeChanged()의 스니펫입니다.

Kotlin

    // Account for padding
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

    // Account for the label
    if (showText) xpad += textWidth

    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

    // Figure out how big we can make the pie.
    val diameter = Math.min(ww, hh)
    

자바

    // Account for padding
    float xpad = (float)(getPaddingLeft() + getPaddingRight());
    float ypad = (float)(getPaddingTop() + getPaddingBottom());

    // Account for the label
    if (showText) xpad += textWidth;

    float ww = (float)w - xpad;
    float hh = (float)h - ypad;

    // Figure out how big we can make the pie.
    float diameter = Math.min(ww, hh);
    

뷰의 레이아웃 매개변수를 더 세밀하게 제어해야 하는 경우 onMeasure()를 구현하세요. 이 메서드의 매개변수는 View.MeasureSpec 값으로서 뷰의 상위 요소가 원하는 뷰의 크기, 뷰의 크기가 최대 값인지 단지 추천 값일 뿐인지 여부를 알려줍니다. 최적화를 위해 이 값은 압축된 정수로 저장되며 View.MeasureSpec의 정적 메서드를 사용하여 각 정수에 저장된 정보를 압축해제합니다.

onMeasure() 구현 예시는 다음과 같습니다. 이 구현에서 PieChart는 원형을 라벨 크기로 만들기에 충분한 영역을 확보하려 합니다.

Kotlin

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Try for a width based on our minimum
        val minw: Int = paddingLeft + paddingRight + suggestedMinimumWidth
        val w: Int = View.resolveSizeAndState(minw, widthMeasureSpec, 1)

        // Whatever the width ends up being, ask for a height that would let the pie
        // get as big as it can
        val minh: Int = View.MeasureSpec.getSize(w) - textWidth.toInt() + paddingBottom + paddingTop
        val h: Int = View.resolveSizeAndState(
                View.MeasureSpec.getSize(w) - textWidth.toInt(),
                heightMeasureSpec,
                0
        )

        setMeasuredDimension(w, h)
    }
    

자바

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       // Try for a width based on our minimum
       int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
       int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

       // Whatever the width ends up being, ask for a height that would let the pie
       // get as big as it can
       int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
       int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0);

       setMeasuredDimension(w, h);
    }
    

이 코드에는 다음과 같은 세 가지 중요 참고 사항이 있습니다.

  • 계산 시 보기의 패딩을 고려합니다. 앞서 언급했듯이 이것은 뷰의 책임입니다.
  • 도우미 메서드 resolveSizeAndState()는 최종 너비 및 높이 값을 만드는 데 사용됩니다. 이 도우미는 원하는 뷰 크기를 onMeasure()에 전달된 사양과 비교하여 적절한 View.MeasureSpec 값을 반환합니다.
  • onMeasure()에는 반환 값이 없습니다. 그 대신에 이 메소드에서는 setMeasuredDimension()을 호출하여 결과를 전달합니다. 이 메서드를 호출하는 것은 필수입니다. 이 호출을 생략하면 View 클래스에서 런타임 예외가 발생합니다.

그리기!

개체 생성 및 측정 코드를 정의하고 나면 onDraw()를 구현할 수 있습니다. 각 뷰에서는 onDraw()를 서로 다르게 구현하지만 다음과 같이 대부분의 뷰에서 공유하는 일반적인 작업이 몇 가지 있습니다.

  • drawText()를 사용해 그리기. setTypeface()를 호출하여 글꼴을 지정하고 setColor()를 호출하여 텍스트 색상을 지정합니다.
  • drawRect(), drawOval()drawArc()를 사용하여 기본 도형 그리기. setStyle()을 호출하여 도형에 색상이 채워지는지, 윤곽선이 표시되는지, 아니면 둘 다인지 여부를 변경합니다.
  • Path 클래스를 사용하여 더 복잡한 도형 그리기. Path 개체에 직선과 곡선을 추가하여 도형을 정의한 다음 drawPath()를 사용하여 도형을 그립니다. 기본 도형과 마찬가지로 setStyle()에 따라 경로에 윤곽선을 표시하거나 색상을 채우거나 둘 다 적용할 수 있습니다.
  • LinearGradient 개체를 만들어 그라데이션 채우기를 정의합니다. setShader()를 호출하여 색상을 채운 도형에서 LinearGradient를 사용합니다.
  • drawBitmap()을 사용하여 비트맵 그리기.

예를 들어 PieChart를 그리는 코드는 다음과 같습니다. 이 코드에서는 텍스트, 선, 도형을 혼용합니다.

Kotlin

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        canvas.apply {
            // Draw the shadow
            drawOval(shadowBounds, shadowPaint)

            // Draw the label text
            drawText(data[mCurrentItem].mLabel, textX, textY, textPaint)

            // Draw the pie slices
            data.forEach {
                piePaint.shader = it.mShader
                drawArc(bounds,
                        360 - it.endAngle,
                        it.endAngle - it.startAngle,
                        true, piePaint)
            }

            // Draw the pointer
            drawLine(textX, pointerY, pointerX, pointerY, textPaint)
            drawCircle(pointerX, pointerY, pointerSize, mTextPaint)
        }
    }
    

자바

    protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);

       // Draw the shadow
       canvas.drawOval(
               shadowBounds,
               shadowPaint
       );

       // Draw the label text
       canvas.drawText(data.get(currentItem).mLabel, textX, textY, textPaint);

       // Draw the pie slices
       for (int i = 0; i < data.size(); ++i) {
           Item it = data.get(i);
           piePaint.setShader(it.shader);
           canvas.drawArc(bounds,
                   360 - it.endAngle,
                   it.endAngle - it.startAngle,
                   true, piePaint);
       }

       // Draw the pointer
       canvas.drawLine(textX, pointerY, pointerX, pointerY, textPaint);
       canvas.drawCircle(pointerX, pointerY, pointerSize, mTextPaint);
    }