Textview-Read The Fucking Source Code 사용자 정의

사용자 정의view의 실현에 대해 많은 대신선배들이 좋은 일지 글을 내놓았다.프로젝트의 수요에 따라 순환 소수를 표시하는 textview 컨트롤러가 필요합니다. 이 글은 실현 과정에서 사용되고 배운 지식을 기록합니다.
삼부작
개발 경험이 있는 사람들은 모두 알고 있다. 사용자 정의view 전시는 세 가지 절차가 필요하다.
단계
콜백 방법
기능 설명
메모
측량하다
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
현재 컨트롤 및 하위 컨트롤을 주로 측정하는 방법
배치
onLayout(boolean changed, int left, int top, int right, int bottom)
하위 컨트롤의 위치를 설정합니다. 리셋 방법은 단순히 하위 컨트롤을 포함하지 않는 컨트롤, 즉 사용자 정의textview, 이미지 뷰와 같은view일 경우 이 방법을 다시 쓸 필요가 없습니다.예를 들어 Linear Layout과 Framlayout, Relative Layout 등은 View Group에서 계승된 것으로 하위 컨트롤을 포함할 수 있는view는 다시 써야 한다
그리다
onDraw(Canvas canvas)
canvas로 현재 컨트롤 그리기
1. 3부작 1측정 - onMeasure
측정의 주요 리셋 방법은 onMeasure(int widthMeasureSpec, int heightMeasureSpec), 호출 시기:
         xml        ,             ,

두 개의 매개변수
  (widthMeasureSpec   heightMeasureSpec)  android  xml          (wrap_content,match_parent,     )   ,         
(      ,           ,               )

이 두 매개 변수 값의 생성은 하나의 클래스에 의해 정의된다. 이 클래스는View의 정적 내부 클래스인 MeasureSpec이기 때문에android가 컨트롤의 넓이를 어떻게 측정하는지 잘 알기 때문에MeasureSpec라는 클래스를 먼저 알아야 한다.
1.1 MeasureSpec 측정 모드
측정할 때, 컨트롤러는 어떻게 하위 컨트롤러를 측정하고, 하위 컨트롤러는 부모 컨트롤러가 필요로 하는 넓이가 얼마인지 알려주며, 만약 부모 컨트롤러의 공간이 부족하다면 어떻게 처리합니까?전반적으로 어떻게 측정하는가이다. 이를 위해 MeasureSpec 클래스에서 측정하는 세 가지 모델을 정의했다.
     /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;

구체적으로 설명하다
패턴

xml 값 예
설명
UNSPECIFIED
0x0<<30
wrap_content
하위 컨트롤의 크기를 지정하지 않습니다. 하위 컨트롤이 필요한 만큼 크기를 제한할 수 있습니다.
AT_MOST
0x1<<30
match_parent(fill_parent)
하위 컨트롤 최대화, 보통 부모 컨트롤 크기
EXACTLY
0x2<<30
100dp 등
부모 컨트롤러는 하위 컨트롤러에 크기 영역을 지정합니다. 파이프 컨트롤러가 원하는 만큼 공간을 주지 않습니다.
1.2 MeasureSpec 구조
우리는 onMeasure 방법이 전송하는 너비 값은 두 개의 int 값인 것을 알고 있다. 이 두 int 값은 어떻게 패턴 값과 컨트롤의 실제 너비를 포함하는지 알고 있다. MeasureSpec 원본을 보았는데 구조 함수가 없는 것을 발견했다. makeSafeMeasureSpec(int size, int mode) 방법으로 하나의 size와 mode를 전송하여 MeasureSpec 값을 구축한 것이다.
 private static final int MODE_MASK  = 0x3 << 30;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }

방법은 간단하다. 결과적으로 int가 되돌아오는 int값이 두 자리 높으면 측정 모드를 나타내고 그 다음은 구체적인 값을 나타낸다. 그리고 MeasureSpec도 두 가지 정적 방법으로 측정 모드(getMode)와 값(getSize)을 얻는다.
따라서 onMeasure(int width Measure Spec, int height Measure Spec)는 전송된 매개 변수 width Measure Spec를 통해 부모 컨트롤이나 xml에 정의된 측정 값과 측정 모드를 얻을 수 있으며 이 방법에서 자신의 측정 모드를 사용자 정의할 수 있다.
프로젝트의 수요로 돌아가 순환수를 그리는 사고방식은 두 부분으로 나뉘는데 하나는 문자의 그리기이고 두 번째 부분은 점의 그리기이기 때문에 측정 코드는 비교적 간단하다. 너비는 문자의 너비이고 높이는 문자의 높이 + 점의 높이 + 점과 문자의 간격이다.여기서 어려운 점은 문자의 넓이가 측정하기 어렵다는 데 있다. 예를 들어 숫자 1과 3은 중국어가 차지하는 넓이와 다르다.실제로android는 우리에게 api를 제공해 주었습니다. 바로 Paint류의 get Text Bounds 방법입니다. String을 전달하고 시작과 끝의 각표를 전달합니다. Bounds 대상은 저장 측정의 경계입니다. 조금만 바꾸면 넓이가 됩니다.
/**
 * Return in bounds (allocated by the caller) the smallest rectangle that
 * encloses all of the characters, with an implied origin at (0,0).
 *
 * @param text string to measure and return its bounds
 * @param start index of the first char in the string to measure
 * @param end 1 past the last char in the string to measure
 * @param bounds returns the unioned bounds of all the text. Must be allocated by the caller
 */
public void getTextBounds(String text, int start, int end, Rect bounds) {
    if ((start | end | (end - start) | (text.length() - end)) < 0) {
        throw new IndexOutOfBoundsException();
    }
    if (bounds == null) {
        throw new NullPointerException("need bounds Rect");
    }
    nGetStringBounds(mNativePaint, mNativeTypeface, text, start, end, mBidiFlags, bounds);
}

그래서 문자의 넓이가 측정되었다. 문자의 높이 + 점의 높이와 점과 문자의 간격이 바로 컨트롤러의 높이이다. 여기는 줄을 바꾸는 것을 고려하지 않기 때문에 문자의 넓이는 컨트롤러의 넓이이다. 구체적인 측정 코드는 다음과 같다.
  @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    mPaint.getTextBounds(mText, 0, mText.length(), mBound);
    setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}

private int measureWidth(int widthMeasureSpec) {
    return resolveSize(mBound.width(),widthMeasureSpec);
}

private int measureHeight(int heightMeasureSpec) {
    return resolveSize(mBound.height() + mDotLineHeight, heightMeasureSpec);
}

resolveSize 방법은 각 측정 모드에서 이 값이 가능한지 호환하는 것입니다. 원본 코드는 다음과 같습니다.
public static int resolveSize(int size, int measureSpec) {
    return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

바로 부모 컨트롤이 주는measure Spec과 우리가 계산한size를 호환하는 것입니다. 만약에 부모 컨트롤이 주는specSize가 우리의 계산치보다 작다면 현재 컨트롤도 부모 컨트롤의 넓이만 높을 뿐입니다. 여기까지 측정은 기본적으로 끝났습니다.(단, 예를 들어 RelativeLayout 상대 레이아웃 등 비교적 복잡한 레이아웃은 여러 번 측정할 수 있다. 왜냐하면 한 번에 하위 컨트롤의 폭과 높이를 측정할 수 없기 때문이다-!)
1.3 측정 보완
Paint 클래스의 get Text Bounds 방법은 String의 최소 간격만 제시했을 뿐 문자와 문자 사이의 간격을 고려하지 않았습니다. 기본적인 문자와 문자 사이의 간격은 글꼴 파일에 의해 결정되기 때문에 이 방법은 정확한 문자의 폭을 측정할 수 없습니다. 문자가 많으면 측정 폭이 작고 일부 문자가 표시되지 않을 수 있습니다.그래서 다음은TextView에서 문자의 폭을 측정하는 방법(Read The Fucking Source Code)android를 제시합니다.text.Layout 클래스의 방법 getDesiredWidth (한 줄의 텍스트에 필요한 폭을 정확하게 보여줄 수 있음)
 /**
 * Return how wide a layout must be in order to display the specified text with one line per
 * paragraph.
 *
 * 

As of O, Uses * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.

*/ public static float getDesiredWidth(CharSequence source, TextPaint paint) { return getDesiredWidth(source, 0, source.length(), paint); }

그래서 측정 방법은 폭을 바꾸는 측정이 필요하다. 코드는 다음과 같다.
private int measureWidth(int widthMeasureSpec) {
    return resolveSize((int) Math.ceil(Layout.getDesiredWidth(mText, mPaint)), widthMeasureSpec);
}   

측정이 완성되고view를 어떻게 놓아야 하는지, 거기에 놓으려면 두 번째 단계인 레이아웃이 필요합니다.
2. 3부작-레이아웃-onLayout
단순한 View라면 View Group 클래스가 아니라면 (child의 사용자 정의view 없음) 레이아웃을 복사할 필요가 없습니다.LinearLayout의 수직 레이아웃이 어떻게 완성되었는지 분석해 봤으면 좋겠습니다.
 public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;

 @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

따라서 만약에 우리가 Linear Layout을 사용하고 Orientation을 지정하지 않으면 기본적으로 가로 레이아웃을 사용합니다. 다음은layoutVertical 방법으로 이루어지고 코드가 비교적 길며 세그먼트 분석을 합니다.
final int paddingLeft = mPaddingLeft;

int childTop;
int childLeft;

// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;

// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;

final int count = getVirtualChildCount();

final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

switch (majorGravity) {
   case Gravity.BOTTOM:
       // mTotalLength contains the padding already
       childTop = mPaddingTop + bottom - top - mTotalLength;
       break;

       // mTotalLength contains the padding already
   case Gravity.CENTER_VERTICAL:
       childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
       break;

   case Gravity.TOP:
   default:
       childTop = mPaddingTop;
       break;
}

이 부분의 코드는 주로padding을 계산한다. 왜냐하면android의 좌표계는 왼쪽 상단이 원점이기 때문이다. 여기서 시작하는childTop이Gravity에 있다는 것을 설명한다.BOTTOM의 계산,Linear Layout에Gravity가 밑부분부터 레이아웃을 설정하면childTop의 계산은 단순한paddingTop이 아니다. 계산 방식은 다음과 같다.
childTop = mPaddingTop + bottom - top - mTotalLength;

이상하다. 한꺼번에 알아볼 수 없구나~ 정상적인 사고방식으로 돌아가면 밑에서부터 레이아웃을 시작하면 첫 번째 차일드의 탑은 Linear Layout의 높이(bottom - top)에서 모든 차일드의 높이와 차일드 사이의 간격을 줄이고 밑의 패딩(mPadding Bottom)을 빼야 한다. 즉,
childTop=bottom - top-(child  +child     )-mPaddingBottom

다시 mTotalLength가 어떤 귀신인지 보자. 이 값은 onMeasure에서 볼 필요가 있다. 여기에 원본 코드를 상세하게 붙이지 않고 분석했다(다음은 관건적인 코드이다). mTotalLength가 세로로 배치할 때
mTotalLength = child  +child      + mPaddingBottom + mPaddingTop

그래서 원본 코드의 이 계산은 문제없다. 단지 약간 빙빙 돌기만 할 뿐이다.다음은 mTotalLength가 세로 레이아웃 아래의 관건적인 계산 부분(마찬가지로 가운데에 child를 놓는 계산을 이해할 수 있다. 그리고padding의 설정이 가운데에 영향을 미치는 것을 발견했다. 즉, paddingtop가 크고 paddingbottom이 작으면Gravity.CENTER VERTICAL의 효과는 바로 밑으로 기울어진다는 것이다).
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    mTotalLength = 0;
    for (int i = 0; i < count; ++i) {
            ... ...
        //   divider   
        if (hasDividerBeforeChildAt(i)) {
            mTotalLength += mDividerHeight;
        }
        ... ...
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
            ......
            //   MeasureSpec.EXACTLY    child   
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            skippedMeasure = true;
        } else {
           ......
           //   child   
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                   lp.bottomMargin + getNextLocationOffset(child));

        }
    }
    ......
    // Add in our padding   LinearLayout padding
    mTotalLength += mPaddingTop + mPaddingBottom;
    ......
}

ok, 첫 번째child의 top 위치를 계산했습니다. 다음은 이 위치에서 하나하나child를 배치하면 됩니다. 다음은layoutVertical 레이아웃child의 부분을 계속 보겠습니다.
for (int i = 0; i < count; i++) {
    final View child = getVirtualChildAt(i);
    if (child == null) {
        childTop += measureNullChild(i);
    } else if (child.getVisibility() != GONE) {
        final int childWidth = child.getMeasuredWidth();
        final int childHeight = child.getMeasuredHeight();

        final LinearLayout.LayoutParams lp =
                (LinearLayout.LayoutParams) child.getLayoutParams();

        int gravity = lp.gravity;
        if (gravity < 0) {
            gravity = minorGravity;
        }
        final int layoutDirection = getLayoutDirection();
        final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                        + lp.leftMargin - lp.rightMargin;
                break;

            case Gravity.RIGHT:
                childLeft = childRight - childWidth - lp.rightMargin;
                break;

            case Gravity.LEFT:
            default:
                childLeft = paddingLeft + lp.leftMargin;
                break;
        }

        if (hasDividerBeforeChildAt(i)) {
            childTop += mDividerHeight;
        }

        childTop += lp.topMargin;
        setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                childWidth, childHeight);
        childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

        i += getChildrenSkipCount(child, i);
    }
}

이 부분은 for 순환으로 모든child의 위치(left,top,right,bottom)를 순환적으로 계산한 다음child의onlayout 방법을 호출하면 레이아웃이 완성된다. 다른 레이아웃, 예를 들어RelativeLayout,FrameLayout 등은 서로 다른 특성 레이아웃을 가지고 이 두 단계에 따라 설정한다.
2.2 LinearLayout의 Divider
LinearLayout에 divider 기능이 있는지 몰랐는데, 원본을 보고 LinearLayout의 Divider 사용법을 발견했어요!!!각child에 구분된 그림을 설정할 수 있습니다.api는 다음과 같습니다.
메서드
설명
setDividerDrawable(Drawable divider)
간격 그림 설정
setDividerPadding(int padding)
간격 설정
setShowDividers(@DividerMode int showDividers)
Divider가 표시하는 모드를 설정합니다. 표시를 시작하거나 끝내거나 child 사이에 표시하거나 표시하지 않습니다.
두 번째 레이아웃도 완성됐습니다.
3 3부작 - 드로잉 - onDraw
onDraw 방법은 내용을 그리는 것입니다. 먼저 몇 가지 질문을 드리겠습니다.
1.onDraw      view    ?  
2.setBackground   xml    Background        onDraw     super.onDraw()     ?
3.         ?  

내용을 그릴 때는 주로 Canvas의api를 사용합니다. 텍스트 그림, 라인 등을 그립니다. 그릴 때 해당하는api를 찾으면 됩니다. 여기서 주의해야 할 것은 canvas입니다.drawText 방법은 문자의 밑부분부터 그려집니다. 세 번째 인자인 y는 문자의 높이, toppadding 등 값을 계산해야 합니다!
순환 소수점을 표시하는 전체 코드를 그립니다.
public class DotNumView extends View {
public DotNumView(Context context) {
    super(context);
    init();
}

public DotNumView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initAttrs(context, attrs);
    init();
}

public DotNumView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initAttrs(context, attrs);
    init();
}

private void initAttrs(Context context, AttributeSet attrs) {
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DotNumView);
    if (ta != null) {
        mTextColor = ta.getColor(R.styleable.DotNumView_android_textColor, DEFAULT_TEXT_COLOR);
        mTextSize = ta.getDimensionPixelSize(R.styleable.DotNumView_android_textSize,
                                             (int) DEFAULT_TEXT_SIZE);
        mText = ta.getString(R.styleable.DotNumView_android_text);
        mDotPadding = ta.getDimension(R.styleable.DotNumView_dotPadding, DEFAULT_DOT_PADDING);
        mDotRadius = ta.getDimension(R.styleable.DotNumView_dotRadius, DEFAULT_DOT_RADIUS);
        ta.recycle();
    }

}


/**
 *       
 */
private static final float DEFAULT_TEXT_SIZE = 30;

/**
 *       
 */
private static final float DEFAULT_DOT_RADIUS = 2;

/**
 *           
 */
private static final float DEFAULT_DOT_PADDING = 5;
private static final int DEFAULT_TEXT_COLOR = Color.BLACK;


/**
 *      padding
 */
private static final float DEFAULT_PADDING_RIGHT = 3;

/**
 *     
 */
float mTextSize = DEFAULT_TEXT_SIZE;

/**
 *    
 */
float mDotRadius = DEFAULT_DOT_RADIUS;

/**
 *        
 */
float mDotPadding = DEFAULT_DOT_PADDING;

/**
 *        
 */
String mText = "";
int mTextColor = DEFAULT_TEXT_COLOR;

/**
 *     
 */
Rect mBound = new Rect();

private TextPaint mPaint;

/**
 *          
 */
private int[] mDotIndexs;


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    mPaint.getTextBounds(mText, 0, mText.length(), mBound);
    setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}

private int measureWidth(int widthMeasureSpec) {
    return resolveSize((int) Math.ceil(Layout.getDesiredWidth(mText, mPaint)), widthMeasureSpec);
}

private int measureHeight(int heightMeasureSpec) {
    return resolveSize((int) (mBound.height() + mDotRadius * 4 + mDotPadding * 4),
                       heightMeasureSpec);
}

private void init() {
    mPaint = new TextPaint();
    mPaint.setColor(mTextColor);
    mPaint.setTextSize(mTextSize);
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.FILL);

    if (mText == null) {
        mText = "";
    }
}

public void setTypeFace(Typeface textFont) {
    mPaint.setTypeface(textFont);
}

/**
 *       
 *
 * @param color
 */
public void setTextColor(int color) {
    mTextColor = color;
    invalidate();
}

/**
 *          
 */
public void setDotPadding(int padding) {
    mDotPadding = padding;
    refresh();
}

/**
 *     
 *
 * @param text
 */
public void setText(String text) {
    if (text == null) {
        return;
    }
    this.mText = text;
    refresh();
}

/**
 *      index          
 *
 * @param text
 */
public void setTextWithDotIndex(String text, int... index) {
    if (text == null) {
        return;
    }
    this.mText = text;
    mDotIndexs = index;
    refresh();
}

private void refresh() {
    requestLayout();
    invalidate();
}

@Override
protected void onDraw(Canvas canvas) {
    //        super.onDraw(canvas);
    mPaint.setColor(mTextColor);
    mPaint.getTextBounds(mText, 0, mText.length(), mBound);
    canvas.drawText(mText, 0, mBound.height() + mDotRadius * 2 + mDotPadding * 2, mPaint);

    if (mDotIndexs == null) {
        return;
    }
    for (int index : mDotIndexs) {
        drawDot(canvas, index);
    }
}

/**
 *    
 *
 * @param canvas
 * @param i
 */
private void drawDot(Canvas canvas, int i) {
    if (i >= mText.length()) {
        return;
    }
    Rect dotBound = new Rect();
    mPaint.getTextBounds(mText, 0, i, dotBound);
    int left = dotBound.right;
    mPaint.getTextBounds(mText, 0, i + 1, dotBound);
    float x = (dotBound.right + left) / 2.0f + mDotRadius;
    canvas.drawCircle(x, mDotRadius + mDotPadding, mDotRadius, mPaint);
}
}

3.1 onDraw가 전부는 아니에요.
여기서 우리는 View의 원본 코드를 보면 된다. View의 draw 방법, 핵심 코드와 주석은 다음과 같다.
 @CallSuper
public void draw(Canvas canvas) {
     ......
    // Step 1, draw the background, if needed
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    .....
    // Step 2, save the canvas' layers
    .....
    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

    // Step 4, draw the children
    dispatchDraw(canvas);

    // Step 5, draw the fade effect and restore layers
    ......
    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);
     .....
}

간단하게 번역해 주세요.
1.    ,      
2.  canvas    
3.    ,     ondraw    
4.  child  
5.       ,  canvas     
6.     ,  scrollbars   

따라서view를 사용자 정의할 때background,ondraw 내용,child,Foreground 등은 층층이 그려져 있고 뒤쪽은 앞쪽을 덮어씁니다(겹치면 겹쳐집니다).실제로 안에 또 있어요.
4. 요약
  • onMeasure 측정은 3가지 모드가 있고 핵심 클래스인 MeasureSpec
  • 문자의 측정은 TextPaint, Layout, Paint류의 getTextBounds를 사용하여 문자의 최소 구역을 획득해야 한다
  • 자주 사용하는 레이아웃(LinearLayout 등)은 모두 onLayout에서 그 특성을 처리한다
  • 글자를 그리는 것은 글자의 밑부분부터 그리는 것이다
  • 그리기 순서:background->onDraw->그리기child->그림자그리기->Foreground
  • 좋은 웹페이지 즐겨찾기