맞춤 뷰에서 가장 중요한 부분은 디자인입니다. 맞춤 그리기는 애플리케이션의 요구사항에 따라 쉽거나 복잡할 수 있습니다. 이 문서에서는 가장 일반적인 작업 몇 가지를 다룹니다.
자세한 내용은 드로어블 개요를 참고하세요.
onDraw() 재정의
맞춤 뷰를 그릴 때 가장 중요한 단계는 onDraw()
메서드를 재정의하는 것입니다. onDraw()
의 매개변수는 뷰에서 자신을 그리는 데 사용할 수 있는 Canvas
객체입니다. Canvas
클래스는 텍스트, 선, 비트맵 및 기타 여러 그래픽 프리미티브를 그리기 위한 메서드를 정의합니다. onDraw()
에서 이러한 메서드를 사용하여 맞춤 사용자 인터페이스 (UI)를 만들 수 있습니다.
먼저 Paint
객체를 만듭니다.
다음 섹션에서는 Paint
에 관해 자세히 설명합니다.
그림 개체 만들기
android.graphics
프레임워크는 그리기를 다음 두 영역으로 나눕니다.
- 그릴 내용(
Canvas
에서 처리) - 그리기 방법(
Paint
에서 처리)
예를 들어 Canvas
는 선을 그리는 메서드를 제공하고 Paint
는 선의 색상을 정의하는 메서드를 제공합니다.
Canvas
에는 직사각형을 그리는 메서드가 있고 Paint
는 직사각형을 색상으로 채울지 또는 비워 둘지 정의합니다.
Canvas
는 화면에 그릴 수 있는 도형을 정의하고, Paint
는 그리는 각 도형의 색상, 스타일, 글꼴 등을 정의합니다.
무언가를 그리기 전에 하나 이상의 Paint
객체를 만듭니다. 다음 예에서는 init
라는 메서드에서 이 작업을 실행합니다. 이 메서드는 자바의 생성자에서 호출되지만 Kotlin에서 인라인으로 초기화할 수 있습니다.
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) }
Java
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()
는 뷰가 차트의 경계 직사각형 및 텍스트 라벨 및 기타 시각적 요소의 상대적 위치를 계산하는 위치입니다.
뷰에 크기가 할당되면 레이아웃 관리자는 크기에 뷰의 패딩이 포함되어 있다고 가정합니다. 뷰의 크기를 계산할 때 패딩 값을 처리합니다. 다음은 이를 실행하는 방법을 보여주는 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) }
Java
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) }
Java
@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()
는 최종 너비 및 높이 값을 만드는 데 사용됩니다. 이 도우미는 뷰의 필요한 크기를onMeasure()
에 전달된 값과 비교하여 적절한View.MeasureSpec
값을 반환합니다. onMeasure()
에는 반환값이 없습니다. 대신 이 메서드는setMeasuredDimension()
를 호출하여 결과를 전달합니다. 이 메서드 호출은 필수입니다. 이 호출을 생략하면View
클래스에서 런타임 예외가 발생합니다.
그리기
객체 생성 및 측정 코드를 정의한 후에는 onDraw()
를 구현할 수 있습니다. 모든 뷰는 onDraw()
를 다르게 구현하지만 대부분의 뷰에서 공유하는 몇 가지 일반적인 작업이 있습니다.
drawText()
를 사용하여 텍스트를 그립니다.setTypeface()
를 호출하여 글꼴을 지정하고setColor()
를 호출하여 텍스트 색상을 지정합니다.drawRect()
,drawOval()
및drawArc()
를 사용하여 기본 도형을 그립니다.setStyle()
를 호출하여 도형에 채우기, 윤곽선 표시, 또는 둘 다인지 여부를 변경합니다.Path
클래스를 사용하여 더 복잡한 도형을 그립니다.Path
객체에 선과 곡선을 추가하여 도형을 정의한 다음drawPath()
를 사용하여 도형을 그립니다. 기본 도형과 마찬가지로setStyle()
에 따라 경로에 윤곽선을 표시하거나 색상을 채우거나 둘 다 지정할 수 있습니다.-
LinearGradient
객체를 만들어 그라데이션 채우기를 정의합니다. 색이 채워진 도형에LinearGradient
를 사용하려면setShader()
를 호출하세요. 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 )
Java
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)에서는 블러, 색상 필터, Android 셰이더 효과와 같은 일반적인 그래픽 효과를 View
객체 및 렌더링 계층 구조에 적용하는 RenderEffect
클래스를 추가합니다. 효과를 내부 및 외부 효과 또는 혼합 효과로 구성된 체인 효과로 결합할 수 있습니다. 이 기능 지원은 기기 처리 성능에 따라 다릅니다.
View.setRenderEffect(RenderEffect)
를 호출하여 View
의 기본 RenderNode
에 효과를 적용할 수도 있습니다.
RenderEffect
객체를 구현하려면 다음을 실행하세요.
view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))
뷰를 프로그래매틱 방식으로 만들거나 XML 레이아웃에서 확장하여 뷰 결합 또는
findViewById()
을 사용하여 검색할 수 있습니다.