View
모든 Android 컨트롤은 View 또는 View의 하위 클래스입니다. 실제로는 Rect로 표시되는 화면의 직사각형 영역을 나타냅니다. 시작점, 너비, 및 height는 View의 너비와 높이를 나타냅니다. 이 네 가지 필드를 통해 화면에서 View의 위치를 결정할 수 있습니다. 위치를 결정한 후 View의 내용을 그릴 수 있습니다.
뷰 그리기 과정
뷰 그리기는 다음 세 가지 과정으로 나눌 수 있습니다.
측정
뷰는 먼저 얼마나 많은 면적을 차지해야 하는지 계산하기 위해 측정을 합니다. View의 Measure 프로세스는 onMeasure 인터페이스를 노출합니다. 메소드 정의는 다음과 같습니다.
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}
View 클래스는 기본 onMeasure 구현인
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }
를 제공합니다.
측정 프로세스 중에 View의 너비와 높이를 설정하기 위해 setMeasuredDimension() 메서드가 호출됩니다. getSuggestedMinimumWidth()는 View의 최소 너비를 반환하며 Height에도 해당 메서드가 있습니다. 간단히 말하면 MeasureSpec 클래스는 View 클래스의 내부 정적 클래스입니다. 이는 UNSPECIFIED, AT_MOST 및 EXACTLY라는 세 가지 상수를 정의합니다. 실제로 이 클래스는 LayoutParams의 match_parent, Wrap_content 및 xxxdp에 해당합니다. . onMeasure를 재정의하여 뷰의 너비와 높이를 재정의할 수 있습니다. <… 하위 뷰가 없습니다. 정렬이 필요하므로 실제로 이 단계에서는 추가 작업을 수행할 필요가 없습니다. 그런데 ViewGroup 클래스의 경우 onLayout 메서드에서 모든 하위 View의 크기, 너비, 높이를 설정해야 합니다. 이에 대해서는 다음 글에서 자세히 설명하겠습니다.
그리기
그리기 프로세스는 캔버스에 필요한 뷰 스타일을 그리는 것입니다. 마찬가지로 View는 onDraw 메소드
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
를 노출합니다. 기본 View 클래스의 onDraw에는 코드 줄이 없지만 예를 들어 그림 스크롤과 같이 빈 캔버스를 제공합니다. 마찬가지로 우리는 화가이고, 우리가 만들어낼 수 있는 효과는 전적으로 우리에게 달려 있습니다.
View에는 세 가지 중요한 메서드가 더 있습니다
requestLayout
View는 그리기 프로세스를 다시 호출합니다.
forceLayout
은 다음에 뷰를 다시 그려야 하고 레이아웃 프로세스를 다시 호출해야 함을 나타냅니다.
우리는 이미 전체 View 그리기 프로세스를 소개했으며 또 다른 매우 중요한 지식인 사용자 정의 컨트롤 속성이 View에 이미 레이아웃_너비, 레이아웃_높이와 같은 몇 가지 기본 속성이 있다는 것을 알고 있습니다. 배경 등. 우리는 종종 우리 자신의 속성을 정의해야 하므로 구체적으로 이를 수행할 수 있습니다.
2. 다음 예에서는 길이 속성 2개와 색상 값 1개를 사용하므로 여기서 먼저 속성 3개를 만듭니다.
protected void onDraw(Canvas canvas) { }
그럼 사용법은 예시를 통해 살펴보도록 하겠습니다.
비교적 간단한 Google Rainbow 진행률 표시줄을 구현합니다.
간단함을 위해 여기서는 한 가지 색상만 사용하고 여러 색상은 여러분에게 맡기고 바로 코드로 넘어가겠습니다.
<declare-styleable name="rainbowbar"> <attr name="rainbowbar_hspace" format="dimension"></attr> <attr name="rainbowbar_vspace" format="dimension"></attr> <attr name="rainbowbar_color" format="color"></attr> </declare-styleable>
View에는 다시 작성해야 하는 세 가지 구성 메서드가 있습니다. 다음은 세 가지 메서드가 호출되는 시나리오입니다.
일반적으로 우리는 이런 식으로 사용될 때 호출됩니다. View view = new View(context);
두 번째 방법은 xml 레이아웃 파일에서 View를 사용할 때 레이아웃을 확장할 때 호출됩니다.
세 번째 방법은 두 번째 방법과 비슷하지만 스타일 속성 설정을 추가합니다. 이때 인플레이터 레이아웃 중에 세 번째 생성자가 호출됩니다.
<스타일 보기="@styles/MyCustomStyle"layout_width="match_parent"layout_height="match_parent"/>.위에서 조금 혼란스러울 수 있는 점은 세 번째 구성 방법에서 사용자 정의 속성 hspace, vspace 및 barcolor를 초기화하고 읽는 코드를 작성했지만 내 RainbowBar가 선형 레이아웃에서 스타일 속성을 추가하지 않았다는 것입니다. .() 위의 설명에 따르면 레이아웃을 확장할 때 두 번째 생성자를 호출해야 하지만 두 번째 생성자에서 세 번째 생성자인 this(context, attrs, 0)를 호출하므로 사용자 정의 속성을 읽는 데 문제가 없습니다. 세 번째 구성 방법에서는 코드 중복을 피하기 위한 작은 세부 사항입니다. -, -
Draw
여기서는 측정 및 레이아웃 프로세스에 주의를 기울일 필요가 없으므로 직접 onDraw를 재정의합니다. 방법.
public class RainbowBar extends View { //progress bar color int barColor = Color.parseColor("#1E88E5"); //every bar segment width int hSpace = Utils.dpToPx(80, getResources()); //every bar segment height int vSpace = Utils.dpToPx(4, getResources()); //space among bars int space = Utils.dpToPx(10, getResources()); float startX = 0; float delta = 10f; Paint mPaint; public RainbowBar(Context context) { super(context); } public RainbowBar(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RainbowBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //read custom attrs TypedArray t = context.obtainStyledAttributes(attrs, R.styleable.rainbowbar, 0, 0); hSpace = t.getDimensionPixelSize(R.styleable.rainbowbar_rainbowbar_hspace, hSpace); vSpace = t.getDimensionPixelOffset(R.styleable.rainbowbar_rainbowbar_vspace, vSpace); barColor = t.getColor(R.styleable.rainbowbar_rainbowbar_color, barColor); t.recycle(); // we should always recycle after used mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setColor(barColor); mPaint.setStrokeWidth(vSpace); } ....... }
//draw be invoke numbers. int index = 0; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //get screen width float sw = this.getMeasuredWidth(); if (startX >= sw + (hSpace + space) - (sw % (hSpace + space))) { startX = 0; } else { startX += delta; } float start = startX; // draw latter parse while (start < sw) { canvas.drawLine(start, 5, start + hSpace, 5, mPaint); start += (hSpace + space); } start = startX - space - hSpace; // draw front parse while (start >= -hSpace) { canvas.drawLine(start, 5, start + hSpace, 5, mPaint); start -= (hSpace + space); } if (index >= 700000) { index = 0; } invalidate(); }
自定义ViewGroup
ViewGroup
我们知道ViewGroup就是View的容器类,我们经常用的LinearLayout,RelativeLayout等都是ViewGroup的子类,因为ViewGroup有很多子View,所以它的整个绘制过程相对于View会复杂一点,但是还是三个步骤measure,layout,draw,我们一次说明。
Measure
Measure过程还是测量ViewGroup的大小,如果layout_widht和layout_height是match_parent或具体的xxxdp,就很简答了,直接调用setMeasuredDimension()方法,设置ViewGroup的宽高即可,如果是wrap_content,就比较麻烦了,我们需要遍历所有的子View,然后对每个子View进行测量,然后根据子View的排列规则,计算出最终ViewGroup的大小。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int childCount = this.getChildCount(); for (int i = 0; i < childCount; i++) { View child = this.getChildAt(i); this.measureChild(child, widthMeasureSpec, heightMeasureSpec); int cw = child.getMeasuredWidth(); // int ch = child.getMeasuredHeight(); } }
你可能需要类似上面的代码,其中getChildCount()方法,返回子View的数量,measureChild()方法,调用子View的测量方法。
Layout
上面View的自定义中,我们稍微提到了,layout过程其实就是对子View的位置进行排列,onLayout方法给我一个机会,来按照我们想要的规则自定义子View排列。
@Override protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) { int childCount = this.getChildCount(); for (int i = 0; i < childCount; i++) { View child = this.getChildAt(i); LayoutParams lParams = (LayoutParams) child.getLayoutParams(); child.layout(lParams.left, lParams.top, lParams.left + childWidth, lParams.top + childHeight); } }
你同样可能需要类似上面的代码,其中child.layout(left,top,right,bottom)方法可以对子View的位置进行设置,四个参数的意思大家通过变量名都应该清楚了。
Draw
ViewGroup在draw阶段,其实就是按照子类的排列顺序,调用子类的onDraw方法,因为我们只是View的容器, 本身一般不需要draw额外的修饰,所以往往在onDraw方法里面,只需要调用ViewGroup的onDraw默认实现方法即可。
LayoutParams
ViewGroup还有一个很重要的知识LayoutParams,LayoutParams存储了子View在加入ViewGroup中时的一些参数信息,在继承ViewGroup类时,一般也需要新建一个新的LayoutParams类,就像SDK中我们熟悉的LinearLayout.LayoutParams,RelativeLayout.LayoutParams类等一样,那么可以这样做,在你定义的ViewGroup子类中,新建一个LayoutParams类继承与ViewGroup.LayoutParams。
public static class LayoutParams extends ViewGroup.LayoutParams { public int left = 0; public int top = 0; public LayoutParams(Context arg0, AttributeSet arg1) { super(arg0, arg1); } public LayoutParams(int arg0, int arg1) { super(arg0, arg1); } public LayoutParams(android.view.ViewGroup.LayoutParams arg0) { super(arg0); } }
那么现在新的LayoutParams类已经有了,如何让我们自定义的ViewGroup使用我们自定义的LayoutParams类来添加子View呢,ViewGroup同样提供了下面这几个方法供我们重写,我们重写返回我们自定义的LayoutParams对象即可。
@Override public android.view.ViewGroup.LayoutParams generateLayoutParams( AttributeSet attrs) { return new NinePhotoView.LayoutParams(getContext(), attrs); } @Override protected android.view.ViewGroup.LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } @Override protected android.view.ViewGroup.LayoutParams generateLayoutParams( android.view.ViewGroup.LayoutParams p) { return new LayoutParams(p); } @Override protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) { return p instanceof NinePhotoView.LayoutParams; }
实例
我们还是做一个实例来说明,我们今天做一个类似微信朋友圈 存储要发送图片的控件,点击+号图片,可以一直加图片,最多9张。那么微信是4个一排,我们这里是3个一排,因为一般常规都是三个一排,这些都是细节不要在意(另外偷偷告诉大家,微信的实现是用TableLayout,-.-)。
public class NinePhotoView extends ViewGroup { public static final int MAX_PHOTO_NUMBER = 9; private int[] constImageIds = { R.drawable.girl_0, R.drawable.girl_1, R.drawable.girl_2, R.drawable.girl_3, R.drawable.girl_4, R.drawable.girl_5, R.drawable.girl_6, R.drawable.girl_7, R.drawable.girl_8 }; // horizontal space among children views int hSpace = Utils.dpToPx(10, getResources()); // vertical space among children views int vSpace = Utils.dpToPx(10, getResources()); // every child view width and height. int childWidth = 0; int childHeight = 0; // store images res id ArrayList<integer> mImageResArrayList = new ArrayList<integer>(9); private View addPhotoView; public NinePhotoView(Context context) { super(context); } public NinePhotoView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NinePhotoView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray t = context.obtainStyledAttributes(attrs, R.styleable.NinePhotoView, 0, 0); hSpace = t.getDimensionPixelSize( R.styleable.NinePhotoView_ninephoto_hspace, hSpace); vSpace = t.getDimensionPixelSize( R.styleable.NinePhotoView_ninephoto_vspace, vSpace); t.recycle(); addPhotoView = new View(context); addView(addPhotoView); mImageResArrayList.add(new integer()); }
目前为止,都跟上一篇说的大致差不多,另外拍照和从相册选择图片不是我们这一篇的重点,所以我们把图片硬编码到代码中(全是美女...),ViewGroup初始化时我们添加了一个+号按钮,给用户点击添加新的图片。
Measure
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int rw = MeasureSpec.getSize(widthMeasureSpec); int rh = MeasureSpec.getSize(heightMeasureSpec); childWidth = (rw - 2 * hSpace) / 3; childHeight = childWidth; int childCount = this.getChildCount(); for (int i = 0; i < childCount; i++) { View child = this.getChildAt(i); //this.measureChild(child, widthMeasureSpec, heightMeasureSpec); LayoutParams lParams = (LayoutParams) child.getLayoutParams(); lParams.left = (i % 3) * (childWidth + hSpace); lParams.top = (i / 3) * (childWidth + vSpace); } int vw = rw; int vh = rh; if (childCount < 3) { vw = childCount * (childWidth + hSpace); } vh = ((childCount + 3) / 3) * (childWidth + vSpace); setMeasuredDimension(vw, vh); }
我们的子View三个一排,而且都是正方形,所以我们上面通过循环很好去得到所有子View的位置,注意我们上面把子View的左上角坐标存储到我们自定义的LayoutParams 的left和top二个字段中,Layout阶段会使用,最后我们算得整个ViewGroup的宽高,调用setMeasuredDimension设置。
Layout
@Override protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) { int childCount = this.getChildCount(); for (int i = 0; i < childCount; i++) { View child = this.getChildAt(i); LayoutParams lParams = (LayoutParams) child.getLayoutParams(); child.layout(lParams.left, lParams.top, lParams.left + childWidth, lParams.top + childHeight); if (i == mImageResArrayList.size() - 1 && mImageResArrayList.size() != MAX_PHOTO_NUMBER) { child.setBackgroundResource(R.drawable.add_photo); child.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { addPhotoBtnClick(); } }); }else { child.setBackgroundResource(constImageIds[i]); child.setOnClickListener(null); } } } public void addPhoto() { if (mImageResArrayList.size() < MAX_PHOTO_NUMBER) { View newChild = new View(getContext()); addView(newChild); mImageResArrayList.add(new integer()); requestLayout(); invalidate(); } } public void addPhotoBtnClick() { final CharSequence[] items = { "Take Photo", "Photo from gallery" }; AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setItems(items, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface arg0, int arg1) { addPhoto(); } }); builder.show(); }
最核心的就是调用layout方法,根据我们measure阶段获得的LayoutParams中的left和top字段,也很好对每个子View进行位置排列。然后判断在图片未达到最大值9张时,默认最后一张是+号图片,然后设置点击事件,弹出对话框供用户选择操作。
Draw
不需要重写,使用ViewGroup默认实现即可。
附上布局文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="40dp" android:orientation="vertical" > <com.sw.demo.widget.NinePhotoView android:id="@+id/photoview" android:layout_width="match_parent" android:layout_height="wrap_content" app:ninephoto_hspace="10dp" app:ninephoto_vspace="10dp" app:rainbowbar_color="@android:color/holo_blue_bright" > </com.sw.demo.widget.NinePhotoView> </LinearLayout>
最后还是加上程序运行的效果图,今天自定义ViewGroup的讲解就这么多了,祝大家每天都有新收获,每天都有好心情~~~
更多Android App开发中自定义View和ViewGroup的实例教程相关文章请关注PHP中文网!