안드로이드 개발 학습의 019 사용자 정의 보기 만들기

34272 단어 android
새 컨트롤을 생성하려면 일반적으로 View 클래스 또는 SurfaceView 클래스를 확장해야 합니다.View 클래스는 Canvas 개체와 일련의 그리기 방법, Paint 클래스를 제공하기 때문에 시각화된 인터페이스를 그릴 수 있습니다.다음에 화면 터치나 버튼을 누르는 사용자 이벤트를 다시 써서 상호작용을 제공할 수 있습니다.View 클래스를 확장하려면 일반적으로 onMeasure 및 onDraw 메소드를 다시 작성해야 합니다.onMeasure 방법에서 새로운 보기는 일련의 주어진 경계 조건에서 차지하는 높이와 너비를 계산합니다.onDraw 방법은 캔버스에 그림을 그리는 데 사용됩니다.다음은 우리가 간단한 예를 통해 어떻게 사용자 정의 보기를 설명하는 것이다
새 클래스는 View 클래스에서 상속됩니다.
public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
 }

캔버스에 도안을 그리려면 펜 (Paint 대상) 이 필요합니다. 여기에 미리 정의한 다음 두 번째 구조 함수에서 그림을 만들고 간단한 설정을 합니다.
 private static final String TAG = MyView.class.getSimpleName();
 private Paint mPaint;

 public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint();
        //    
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(30);
        //    
        mPaint.setStrokeWidth(3);

    }

컨트롤 크기 조정/다시 쓰기 onMeasure 방법
컨트롤의 부모 용기가 하위 컨트롤을 레이아웃할 때 onMeasure 방법을 호출합니다.그것은 "당신은 얼마나 큰 공간을 사용해야 합니까?"라고 제기했다.이러한 질문은 두 개의 매개 변수를 동시에 전달합니다:widthMeasureSpec와heightMeasureSpec.아래와 같다
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

이 방법을 다시 쓰기 전에 Google의 TextView가 어떻게 이루어졌는지 살펴봅시다.
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;

        BoringLayout.Metrics boring = UNKNOWN_BORING;
        BoringLayout.Metrics hintBoring = UNKNOWN_BORING;

        if (mTextDir == null) {
            mTextDir = getTextDirectionHeuristic();
        }

        int des = -1;
        boolean fromexisting = false;

        if (widthMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            width = widthSize;
        } else {
            if (mLayout != null && mEllipsize == null) {
                des = desired(mLayout);
            }

            if (des < 0) {
                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
                if (boring != null) {
                    mBoring = boring;
                }
            } else {
                fromexisting = true;
            }

            if (boring == null || boring == UNKNOWN_BORING) {
                if (des < 0) {
                    des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
                }
                width = des;
            } else {
                width = boring.width;
            }

            final Drawables dr = mDrawables;
            if (dr != null) {
                width = Math.max(width, dr.mDrawableWidthTop);
                width = Math.max(width, dr.mDrawableWidthBottom);
            }

            if (mHint != null) {
                int hintDes = -1;
                int hintWidth;

                if (mHintLayout != null && mEllipsize == null) {
                    hintDes = desired(mHintLayout);
                }

                if (hintDes < 0) {
                    hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
                    if (hintBoring != null) {
                        mHintBoring = hintBoring;
                    }
                }

                if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
                    if (hintDes < 0) {
                        hintDes = (int) Math.ceil(Layout.getDesiredWidth(mHint, mTextPaint));
                    }
                    hintWidth = hintDes;
                } else {
                    hintWidth = hintBoring.width;
                }

                if (hintWidth > width) {
                    width = hintWidth;
                }
            }

            width += getCompoundPaddingLeft() + getCompoundPaddingRight();

            if (mMaxWidthMode == EMS) {
                width = Math.min(width, mMaxWidth * getLineHeight());
            } else {
                width = Math.min(width, mMaxWidth);
            }

            if (mMinWidthMode == EMS) {
                width = Math.max(width, mMinWidth * getLineHeight());
            } else {
                width = Math.max(width, mMinWidth);
            }

            // Check against our minimum width
            width = Math.max(width, getSuggestedMinimumWidth());

            if (widthMode == MeasureSpec.AT_MOST) {
                width = Math.min(widthSize, width);
            }
        }

        int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
        int unpaddedWidth = want;

        if (mHorizontallyScrolling) want = VERY_WIDE;

        int hintWant = want;
        int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();

        if (mLayout == null) {
            makeNewLayout(want, hintWant, boring, hintBoring,
                          width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
        } else {
            final boolean layoutChanged = (mLayout.getWidth() != want) ||
                    (hintWidth != hintWant) ||
                    (mLayout.getEllipsizedWidth() !=
                            width - getCompoundPaddingLeft() - getCompoundPaddingRight());

            final boolean widthChanged = (mHint == null) &&
                    (mEllipsize == null) &&
                    (want > mLayout.getWidth()) &&
                    (mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));

            final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);

            if (layoutChanged || maximumChanged) {
                if (!maximumChanged && widthChanged) {
                    mLayout.increaseWidthTo(want);
                } else {
                    makeNewLayout(want, hintWant, boring, hintBoring,
                            width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
                }
            } else {
                // Nothing has changed
            }
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            height = heightSize;
            mDesiredHeightAtMeasure = -1;
        } else {
            int desired = getDesiredHeight();

            height = desired;
            mDesiredHeightAtMeasure = desired;

            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(desired, heightSize);
            }
        }

        int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
        if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
            unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
        }

        /*
         * We didn't let makeNewLayout() register to bring the cursor into view,
         * so do it here if there is any possibility that it is needed.
         */
        if (mMovement != null ||
            mLayout.getWidth() > unpaddedWidth ||
            mLayout.getHeight() > unpaddedHeight) {
            registerForPreDraw();
        } else {
            scrollTo(0, 0);
        }

        setMeasuredDimension(width, height);
    }

기절, 간단한 TextView의 onMeasure 코드가 매우 길다. 우리는 이 방법을 세심하게 분석한 결과 크게 세 부분으로 나뉘었다. 먼저 MeasureSpec이라는 종류를 통해 길이와 크기를 얻은 다음에 일련의 판단에 따라 실제 길이와 넓이를 얻었다. 마지막으로 setMeasured Dimension(width, height) 방법으로 컨트롤의 실제 넓이와 높이를 설정했다.이 메이저스펙은 또 뭔가요?저희가 이런 걸 알아야 될 것 같아요.
MeasureSpec 클래스 소개
이것은 View의 내부 클래스입니다. 정의는 다음과 같습니다.
    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * 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;

        /**
         * Creates a measure specification based on the supplied size and mode.
         *
         * The mode must always be one of the following:
         * 
    *
  • {@link android.view.View.MeasureSpec#UNSPECIFIED}
  • *
  • {@link android.view.View.MeasureSpec#EXACTLY}
  • *
  • {@link android.view.View.MeasureSpec#AT_MOST}
  • *
* *

Note: On API level 17 and lower, makeMeasureSpec's * implementation was such that the order of arguments did not matter * and overflow in either value could impact the resulting MeasureSpec. * {@link android.widget.RelativeLayout} was affected by this bug. * Apps targeting API levels greater than 17 will get the fixed, more strict * behavior.

* * @param size the size of the measure specification * @param mode the mode of the measure specification * @return the measure specification based on size and mode */
public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } /** * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED * will automatically get a size of 0. Older apps expect this. * * @hide internal use only for compatibility with system widgets and older apps */ public static int makeSafeMeasureSpec(int size, int mode) { if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) { return 0; } return makeMeasureSpec(size, mode); } /** * Extracts the mode from the supplied measure specification. * * @param measureSpec the measure specification to extract the mode from * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, * {@link android.view.View.MeasureSpec#AT_MOST} or * {@link android.view.View.MeasureSpec#EXACTLY} */ public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } /** * Extracts the size from the supplied measure specification. * * @param measureSpec the measure specification to extract the size from * @return the size in pixels defined in the supplied measure specification */ public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } static int adjust(int measureSpec, int delta) { final int mode = getMode(measureSpec); int size = getSize(measureSpec); if (mode == UNSPECIFIED) { // No need to adjust size for UNSPECIFIED mode. return makeMeasureSpec(size, UNSPECIFIED); } size += delta; if (size < 0) { Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size + ") spec: " + toString(measureSpec) + " delta: " + delta); size = 0; } return makeMeasureSpec(size, mode); } /** * Returns a String representation of the specified measure * specification. * * @param measureSpec the measure specification to convert to a String * @return a String with the following format: "MeasureSpec: MODE SIZE" */ public static String toString(int measureSpec) { int mode = getMode(measureSpec); int size = getSize(measureSpec); StringBuilder sb = new StringBuilder("MeasureSpec: "); if (mode == UNSPECIFIED) sb.append("UNSPECIFIED "); else if (mode == EXACTLY) sb.append("EXACTLY "); else if (mode == AT_MOST) sb.append("AT_MOST "); else sb.append(mode).append(" "); sb.append(size); return sb.toString(); } }

이 도구류는 주로 네 가지 방법과 세 가지 상수가 있다. UNSPECIFIED: 0 왼쪽으로 30자리, 최고 두 자리는 00 EXACTLY: 1 왼쪽으로 30자리, 최고 두 자리는 01 ATMOST:2 왼쪽으로 30, 가장 높은 두 자리는 10
//이것은 우리가 제시한 사이즈 크기와 패턴으로 이 두 가지 정보를 포함하는 int 변수를 생성합니다. 여기 이 패턴의 매개 변수는 세 개의 상수 중 하나를 전달합니다.public static int makeMeasureSpec(int size, int mode)
//이것은 이 변수에 표시된 패턴 정보를 얻어 세 개의 상수 중 하나를 되돌려준다.public static int getMode(int measureSpec)
//이것은 이 변수에 표시된 사이즈 크기를 얻는 값이다.public static int getSize(int measureSpec)
//로그public static String toString (int measure Spec) 을 쉽게 칠 수 있도록 이 변수의 패턴과 크기를 문자열로 되돌려줍니다.
TextView의 onMeasure 방법에서 볼 수 있습니다.
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int형 변수를 어떻게 모드와 사이즈 값을 얻을 수 있습니까?비결은 int형 정수가 32자리인데 가장 높은 2자리 표시 모드를 나타낸다. 그 다음에 30자리는 크기가 어떻게 한 정수의 가장 높은 두 자리를 얻을 수 있는지를 나타낸다. 11000000000000000000000000000000000000000000000000000000000000000과 위치를 바꾸어 얻을 수 있다.
최고 두 자리는 00일 때'미지정 모드'를 나타낸다.즉, MeasureSpec.UNSPECIFIED의 최고 두 자리는 01에'정밀모드'를 나타낸다.즉, MeasureSpec.EXACTLY 최대 두 자리는 11에'최대 모드'를 나타낸다.즉, MeasureSpec.AT_MOST
그러면 어떻게 30자리의 수치를 얻습니까?01111111111111111111111111111111와 함께
그렇다면 세 가지 모델은 구체적으로 무엇을 대표하는가?UNSPECIFIED: 모 레이아웃은 자 레이아웃에 아무런 제한이 없으며 자 레이아웃은 임의의 크기로 할 수 있습니다.예를 들어 컨트롤 Scroll View/Horizontal Scroll View는 EXACTLY입니다. 부모 레이아웃은 하위 요소의 정확한 크기를 결정하고 하위 요소는 주어진 경계에 한정되며 그 자체의 크기를 무시합니다.보통 레이아웃width가 명확한 값을 설정하거나 MATCHPARENT AT_MOST: 하위 레이아웃은 크기에 따라 원하는 크기를 선택할 수 있습니다.일반 layoutwidth는 WARP 입니다.CONTENT
MeasureSpec 클래스에 대해 기본적으로 알고 난 후, 우리는 온 Measure 방법을 다시 쓰기 시작했다
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //       
        int measuredWidth = measureWidth(widthMeasureSpec);
        int measuredHeight = measureHeight(heightMeasureSpec);
        //    setMeasuredDimension,                  
        setMeasuredDimension(measuredWidth, measuredHeight);
    }

    private int measureHeight(int heightMeasureSpec) {
        //      
        int result = 200;
        //        
        int padding = getPaddingTop() + getPaddingBottom();
        //    
        int specMode = MeasureSpec.getMode(heightMeasureSpec);
        //     
        int specSize = MeasureSpec.getSize(heightMeasureSpec);
        //        
        Log.i(TAG,"     " + MeasureSpec.toString(heightMeasureSpec));

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                break;
            case MeasureSpec.AT_MOST:
                //result = specSize;
                //   ondraw            。            
                result = Math.min(heightMeasureSpec,measureTextHeight()+padding);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    private int measureWidth(int widthMeasureSpec) {
        //      
        int result = 2000;
        //        
        int padding = getPaddingLeft() + getPaddingRight();
        //    
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        //     
        int specSize = MeasureSpec.getSize(widthMeasureSpec);
        //        
        Log.i(TAG, "     " + MeasureSpec.toString(widthMeasureSpec));
        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                break;
            case MeasureSpec.AT_MOST:
                result = Math.min(widthMeasureSpec,specSize+padding);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }
 //      
    private int measureTextHeight() {
        Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
//        System.out.println("fontMetrics = " + fontMetrics);
        return fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading;
    }

onDraw 다시 쓰기 방법
본문은 단지 화포에 문자열을 한 줄 썼을 뿐, 비교적 간단하다.
 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText("MyView", 0, getHeight(), mPaint);
    }

사용자 정의 보기를 사용하고 세 가지 모드를 검증합니다
XML에 사용자 정의 뷰 4개 추가하기

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.antex.myview.MainActivity"
    tools:showIn="@layout/activity_main">

    <HorizontalScrollView android:layout_width="wrap_content"
                          android:layout_height="wrap_content">
        <com.antex.myview.MyView
            android:layout_width="wrap_content"
            android:layout_height="60dp"
            android:text="Hello World!"/>
    HorizontalScrollView>

    <com.antex.myview.MyView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingTop="20dp"
        android:text="Hello World!"/>
    <com.antex.myview.MyView
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="Hello World!"/>
    <com.antex.myview.MyView
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:text="Hello World!"/>
LinearLayout>

첫 번째 보기는 Horizontal Scroll View에 감싸여 있고 부모 보기의 너비는 정해지지 않습니다. 그러면 첫 번째 보기의 너비 모드는 UNSPECIFIED 두 번째 보기의layoutwidth=”wrap_콘텐츠MOST 세 번째 및 네 번째 뷰의 layoutwidth를 match 로 지정parent의 정확한 값이 무엇인지, 그 너비 모드는 EXACTLY이어야 한다.
다음 프로그램을 실행하여 이 결론을 검증합니다. 출력 로그는 12-19 23:13:54.822 2101-2101/?I/MyView: 측정 너비 MeasureSpec: UNSPECIFIED 0 12-19 23:13:54.822 2101-2101/?I/MyView: 높이 측정 MeasureSpec: EXACTLY 158 12-19 23:13:54.822 2101-2101/?I/MyView: 너비 측정 MeasureSpec: ATMOST 996 12-19 23:13:54.822 2101-2101/? I/MyView: 높이 측정 MeasureSpec: ATMOST 1489 12-19 23:13:54.822 2101-2101/? I/MyView: 측정 너비 MeasureSpec: EXACTLY 996 12-19 23:13:54.822 2101-2101/?I/MyView: 높이 측정 MeasureSpec: EXACTLY 158 12-19 23:13:54.822 2101-2101/?I/MyView: 측정 너비 MeasureSpec: EXACTLY 263 12-19 23:13:54.822 2101-2101/?I/MyView: 고도 측정 MeasureSpec: EXACTLY 158 로그를 통해 우리가 생각한 것과 일치하는 것을 발견할 수 있습니다~~~
자, 본문은 여기까지 소개하고 자신만의 보기를 만들기 시작합시다.다음은 사용자 정의 View의 이벤트 상호 작용과 Canvas 상세 설명
개발 도구: Android Studio1.5 SDK: Android 6.0 API 23
코드 다운로드: 019MyView_onMeasure

좋은 웹페이지 즐겨찾기