맞춤 그림 만들기

Compose 방식 사용해 보기
Jetpack Compose는 Android에 권장되는 UI 도구 키트입니다. Compose에서 레이아웃을 사용하는 방법을 알아보세요.
<ph type="x-smartling-placeholder"></ph> Compose의 캔버스 → 를 통해 개인정보처리방침을 정의할 수 있습니다.

맞춤 뷰에서 가장 중요한 부분은 디자인입니다. 맞춤 그리기 복잡할 수 있습니다 이 문서 에서는 가장 일반적인 작업을 다룹니다.

자세한 내용은 드로어블 개요

onDraw() 재정의

맞춤 뷰를 그릴 때 가장 중요한 단계는 onDraw() 메서드를 사용하여 축소하도록 요청합니다. onDraw()의 매개변수는 다음과 같습니다. Canvas 뷰에서 자신을 그리는 데 사용할 수 있는 객체를 정의합니다. Canvas 클래스 텍스트, 선, 비트맵 및 기타 여러 그래픽을 그리기 위한 메서드를 정의합니다. 프리미티브입니다. onDraw()에서 이러한 메서드를 사용하여 커스텀 사용자 인터페이스 (UI)를 제공합니다.

먼저 Paint 객체. 다음 섹션에서는 Paint에 관해 자세히 설명합니다.

그리기 객체 만들기

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

  • 그릴 내용(Canvas에서 처리)
  • 그리기 방법(Paint에서 처리)

예를 들어 Canvas는 선을 그리는 메서드를 제공합니다. Paint는 이 선의 색상을 정의하는 메서드를 제공합니다. Canvas에는 직사각형을 그리는 메서드가 있습니다. Paint 는 직사각형을 색상으로 채울지 또는 비워 둘지를 정의합니다. Canvas는 화면에 그릴 수 있는 도형을 정의합니다. Paint는 각 도형의 색상, 스타일, 글꼴 등을 정의합니다. 있습니다.

무언가를 그리기 전에 하나 이상의 Paint 객체를 만듭니다. 이 다음 예는 init라는 메서드에서 이 작업을 실행합니다. 이 메서드는 Java의 생성자로부터 호출되지만 다음 위치에서 인라인으로 초기화될 수 있습니다. 있습니다.

Kotlin

@ColorInt
private var textColor    // Obtained from style attributes.

@Dimension
private var textHeight   // Obtained from style attributes.

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 Paint textPaint;
private Paint piePaint;
private Paint shadowPaint;

@ColorInt
private int textColor;       // Obtained from style attributes.

@Dimension
private float textHeight;    // Obtained from style attributes.

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(): 그릴 때마다 다시 계산하는 대신 다음 예에서 onSizeChanged()는 뷰가 는 차트의 경계 직사각형과 텍스트 라벨 및 기타 시각적 요소를 포함합니다.

뷰에 크기가 할당되면 Layout Manager는 뷰의 크기가 뷰의 패딩을 포함합니다. 패딩 값을 처리할 때는 뷰의 크기에 따라 달라집니다. 다음은 onSizeChanged()의 스니펫으로, 방법은 다음과 같습니다.

Kotlin

private val showText    // Obtained from styled attributes.
private val textWidth   // Obtained from styled attributes.

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    // Account for padding.
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

    // Account for the label.
    if (showText) xpad += textWidth.toFloat()
    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

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

자바

private Boolean showText;    // Obtained from styled attributes.
private int textWidth;       // Obtained from styled attributes.

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    // 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 you can make the pie.
    float diameter = Math.min(ww, hh);
}

뷰의 레이아웃 매개변수를 더 세밀하게 제어하려면 다음과 같이 구현합니다. onMeasure() 이 메서드의 매개변수는 View.MeasureSpec 뷰의 상위 요소가 원하는 뷰의 크기를 알려주는 값과 그 크기가 하드 최대값이든 단순한 제안이든 상관없습니다. 최적화의 차원에서 이러한 값은 압축된 정수로 저장되며 View.MeasureSpec: 각 정수에 저장된 정보를 압축해제합니다.

다음은 onMeasure() 구현의 예입니다. 이 차트가 최대한 커지도록 영역을 충분히 넓히려고 합니다. 를 라벨로 지정합니다.

Kotlin

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

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

    setMeasuredDimension(w, h)
}

자바

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

   // Whatever the width is, ask for a height that lets the pie get as big as it
   // can.
   int minh = MeasureSpec.getSize(w) - (int)textWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(minh, heightMeasureSpec, 0);

   setMeasuredDimension(w, h);
}

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

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

그리기

객체 생성 및 측정 코드를 정의한 후에는 onDraw() 모든 뷰는 onDraw()를 다르게 구현합니다. 그러나 대부분의 뷰에서 공유하는 몇 가지 일반적인 작업이 있습니다.

  • 다음을 사용하여 텍스트 그리기 drawText() 다음을 호출하여 서체 지정 setTypeface() 을 호출하여 텍스트 색상을 지정합니다. setColor()입니다.
  • 다음을 사용하여 기본 도형 그리기 drawRect(), drawOval(), 및 drawArc()입니다. setStyle()
  • 다음을 사용하여 더 복잡한 도형 그리기: Path 클래스에 대해 자세히 알아보세요. Path에 선과 곡선을 추가하여 도형 정의 객체를 만들고, 도형을 사용하여 도형을 그립니다. drawPath()입니다. 기본 도형과 마찬가지로 경로에 윤곽선을 표시하거나 색상을 채우거나 둘 다 적용할 수 있습니다. setStyle()에 따라 다름
  • LinearGradient를 만들어 그라데이션 채우기 정의 객체입니다. 전화걸기 setShader() 을 사용하여 색이 채워진 도형에 LinearGradient를 사용합니다.
  • 다음을 사용하여 비트맵 그리기 drawBitmap()

다음 코드는 텍스트와 선, 도형을 혼합하여 그립니다.

Kotlin

private val data = mutableListOf<Item>() // A list of items that are displayed.

private var shadowBounds = RectF()       // Calculated in onSizeChanged.
private var pointerRadius: Float = 2f    // Obtained from styled attributes.
private var pointerX: Float = 0f         // Calculated in onSizeChanged.
private var pointerY: Float = 0f         // Calculated in onSizeChanged.
private var textX: Float = 0f            // Calculated in onSizeChanged.
private var textY: Float = 0f            // Calculated in onSizeChanged.
private var bounds = RectF()             // Calculated in onSizeChanged.
private var currentItem: Int = 0         // The index of the currently selected item.

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

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

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

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

        // Draw the pointer.
        drawLine(textX, pointerY, pointerX, pointerY, textPaint)
        drawCircle(pointerX, pointerY, pointerRadius, textPaint)
    }
}

// Maintains the state for a data item.
private data class Item(
    var label: String,      
    var value: Float = 0f,

    @ColorInt
    var color: Int = 0,

    // Computed values.
    var startAngle: Float = 0f,
    var endAngle: Float = 0f,

    var shader: Shader
)

자바

private List<Item> data = new ArrayList<Item>();  // A list of items that are displayed.

private RectF shadowBounds;                       // Calculated in onSizeChanged.
private float pointerRadius;                      // Obtained from styled attributes.
private float pointerX;                           // Calculated in onSizeChanged.
private float pointerY;                           // Calculated in onSizeChanged.
private float textX;                              // Calculated in onSizeChanged.
private float textY;                              // Calculated in onSizeChanged.
private RectF bounds;                             // Calculated in onSizeChanged.
private int currentItem = 0;                      // The index of the currently selected item.

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

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

    // Draw the label text.
    canvas.drawText(data.get(currentItem).label, 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, pointerRadius, textPaint);
}

// Maintains the state for a data item.
private class Item {
    public String label;
    public float value;
    @ColorInt
    public int color;

    // Computed values.
    public int startAngle;
    public int endAngle;

    public Shader shader;
}    

그래픽 효과 적용

Android 12 (API 수준 31)에서는 RenderEffect 클래스가 포함되어 있습니다. 블러, 색상 필터, Android 셰이더 효과 등 View 객체 및 계층 구조를 제공합니다 효과를 연쇄 효과로 결합할 수 있습니다. 연쇄 효과는 혼합 효과를 적용할 수 있습니다. 이 기능 지원 기기 처리 능력에 따라 달라집니다.

또한 기본 레이어에 효과를 RenderNode: 다음을 호출하여 View View.setRenderEffect(RenderEffect)입니다.

RenderEffect 객체를 구현하려면 다음을 실행합니다.

view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))

프로그래밍 방식으로 뷰를 생성하거나 XML 레이아웃에서 뷰를 확장할 수 있습니다. 뷰 결합 또는 findViewById()