본문 바로가기

나의 플랫폼/안드로이드

[Android] Make CustomView (TextView, OnMeasure)

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

CustomView를 만들어 보고자 합니다.

하고자 하는 것은 TextView Background 에 기본적으로 RoundRect 가 그려지도록 할 예정입니다.


여기서 필요한 기술이 두가지 입니다.


1. Background를 어떻게 그리지??

2. CustomeView의 크기를 어떻게 조절하지???


모든것을 설명해 드릴 수 없지만, 개발하는데 유용할 만한 내용은 될것이라 생각 됩니다.


먼저, Background에 이미지를 넣을때 아래와 같은 방법을 많이 씁니다. (전..그랬음.._)


- FrameLayout으로 감싼 후, Background로 활용할 이미지를 ImageView에 먼저 그린 후

  그 다음으로 위에 표현하고자 하는 View를 올린다.

   단점 ) View의 크기가 고정 되어 있지 않으면 (예를 들어 TextView와 같은 유동적인 View들)

             Background ImageView의 크기를 맞추기 어렵다.


- View Background에 해당 이미지를 넣는다. 

   단점) Background가 고정이 된다. 유동적으로 색깔이 변하고 싶거나 모양이 변하고자 할 경우

           표현이 어렵다.



위 방법으로 먼가 표현이 어려울 경우 아래와 같이 한번 해보세요.


- CustomView를 만들어서 onDraw에 canvas를 이용하여 그림을 그린다.


public class GroupTitleView extends TextView {
private int strokeWidth = 10;
private int color;

public GroupTitleView(Context context) {
super(context);
color = Color.RED;
strokeWidth = context.getResources().getDimensionPixelSize(R.dimen._2dp);
}

public GroupTitleView(Context context, AttributeSet attrs) {
super(context, attrs);
color = Color.RED;
strokeWidth = context.getResources().getDimensionPixelSize(R.dimen._2dp);
}


@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint pnt = new Paint();
pnt.setStyle(Paint.Style.STROKE);
pnt.setStrokeWidth(strokeWidth);
pnt.setColor(color);
pnt.setDither(true); // set the dither to true
pnt.setStyle(Paint.Style.STROKE); // set to STOKE
pnt.setStrokeJoin(Paint.Join.ROUND); // set the join to round you want
pnt.setStrokeCap(Paint.Cap.ROUND); // set the paint cap to round too
pnt.setPathEffect(new CornerPathEffect(strokeWidth)); // set the path effect when they join.
pnt.setAntiAlias(true);

Path path = new Path();
final RectF oval = new RectF();
oval.set(strokeWidth, strokeWidth, getWidth()-strokeWidth, getHeight()-strokeWidth);
path.addRoundRect(oval, canvas.getHeight(), canvas.getHeight(), Path.Direction.CCW);

canvas.drawPath(path, pnt);
}

}

위와 같이 그리게 되면 백그라운드에 빨간색 RoundRect가 그려집니다.

하지만, 결과는 아래와 같습니다.




TextView의 text와 백그라운드 그림이 겹쳐버리는 거지요.

이럴 경우, 크기를 다시 조절해줘야 합니다.



package com.test.canvastest;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.widget.TextView;

/**
* Created by Chcheung on 2016-01-21.
*/
public class GroupTitleView extends TextView {
private int strokeWidth = 10;
private int color;

public GroupTitleView(Context context) {
super(context);
color = Color.RED;
strokeWidth = context.getResources().getDimensionPixelSize(R.dimen._2dp);
}

public GroupTitleView(Context context, AttributeSet attrs) {
super(context, attrs);
color = Color.RED;
strokeWidth = context.getResources().getDimensionPixelSize(R.dimen._2dp);
}


@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint pnt = new Paint();
pnt.setStyle(Paint.Style.STROKE);
pnt.setStrokeWidth(strokeWidth);
pnt.setColor(color);
pnt.setDither(true); // set the dither to true
pnt.setStyle(Paint.Style.STROKE); // set to STOKE
pnt.setStrokeJoin(Paint.Join.ROUND); // set the join to round you want
pnt.setStrokeCap(Paint.Cap.ROUND); // set the paint cap to round too
pnt.setPathEffect(new CornerPathEffect(strokeWidth)); // set the path effect when they join.
pnt.setAntiAlias(true);

Path path = new Path();
final RectF oval = new RectF();
oval.set(strokeWidth, strokeWidth, getWidth()-strokeWidth, getHeight()-strokeWidth);
path.addRoundRect(oval, canvas.getHeight(), canvas.getHeight(), Path.Direction.CCW);

canvas.drawPath(path, pnt);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

int w = 0, h = 0;
// ceil not round - avoid thin vertical gaps along the left/right edges
int minw = getPaddingLeft()+getPaddingRight()+getSuggestedMinimumWidth()+getMeasuredWidth()
+(int)(getTextSize()*2)+strokeWidth*2;
int minh = getPaddingTop()+getPaddingBottom()+getSuggestedMinimumHeight()+getMeasuredHeight()
+(int)(getTextSize()*2)+strokeWidth*2;

final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
switch (widthMode) {
case MeasureSpec.AT_MOST://wrap_content
w = minw;
break;
case MeasureSpec.UNSPECIFIED://unspecified
w = widthMeasureSpec;
break;
case MeasureSpec.EXACTLY://match_parent
w = MeasureSpec.getSize(widthMeasureSpec);
break;
}

switch (heightMode) {
case MeasureSpec.AT_MOST:
h = minh;
break;
case MeasureSpec.UNSPECIFIED:
h = heightMeasureSpec;
break;
case MeasureSpec.EXACTLY:
h = MeasureSpec.getSize(heightMeasureSpec);
break;
}
setMeasuredDimension(w, h);
}

}


그래서 수정한 소스 입니다.

그리고 아래는 결과 화면 입니다.




크기 조절은 그냥 제가 임의로 한 거기 때문에 공식이 맞지 않다고 생각 하시는 분은 수정 해서 사용해보세요~


OnMeasure 함수... 먼가 StackOverflow에서 많이 보신 함수 일 껍니다.

이 함수 어떻게 수정 해야 할지 고민이 많았습니다. 

그래서 느낀점이 딱 이 공식을 얻으면 될 것 같습니다.


1. super.onMeasure(widthMeasureSpec, heightMeasureSpec); 함수를 호출하고 난 다음에

   getMeasuredWidth와 getMeasuredHeight는 해당 View 사이즈를 넘겨 준다.


2. 매개변수로 넘어오는  widthMeasureSpec과 heightMeasureSpec은 Parent의 크기를 넘겨 준다.

   즉, View가 추가된 부모 View나 Layout의 사이즈 Spec을 넘겨줍니다. (padding, margin 다 넘어옴)


3. MeasureSpec.AT_MOST : wrap_content 사이즈로

   MeasureSpec.EXACTLE : match_parent 사이즈로

   MeasureSpec.UNSPECIFIED : 0dp나 먼가 값이 정해지지 않은 상태

   -> 위 상태는 부모의 상태를 주는 것이므로, 그 상태에 따라서 대응을 해주면 되겠습니다.



그리고 마지막에 꼭 setMeasureDimension 으로 크기를 지정해줘야 합니다.


함수 호출 순서는 아래와 같습니다.


View.measure() >> View.onMeasure() >> View.setMeasuredDimesion()

출처 : http://i5on9i.blogspot.kr/2013/05/android-view-onmeasure-onlayout.html



위 내용만 알면~ CustomView를 만들거나 소스를 분석할 때 좋은 자료가 될꺼라 생각됩니다.

참고하세요.


참고 사이트: http://stackoverflow.com/a/12267248/3534559


CanvasTest.zip  소스는 첨부 하였습니다.