View
Android所有的控件都是View或者View的子类,它其实表示的就是屏幕上的一块矩形区域,用一个Rect来表示,left,top表示View相对于它的parent View的起点,width,height表示View自己的宽高,通过这4个字段就能确定View在屏幕上的位置,确定位置后就可以开始绘制View的内容了。
View绘制过程
View的绘制可以分为下面三个过程:
Measure
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; }
其中invoke了setMeasuredDimension()方法,设置了measure过程中View的宽高,getSuggestedMinimumWidth()返回View的最小Width,Height也有对应的方法。插几句,MeasureSpec类是View类的一个内部静态类,它定义了三个常量UNSPECIFIED、AT_MOST、EXACTLY,其实我们可以这样理解它,它们分别对应LayoutParams中match_parent、wrap_content、xxxdp。我们可以重写onMeasure来重新定义View的宽高。
Layout
Layout过程对于View类非常简单,同样View给我们暴露了onLayout方法
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
因为我们现在讨论的是View,没有子View需要排列,所以这一步其实我们不需要做额外的工作。插一句,对ViewGroup类,onLayout方法中,我们需要将所有子View的大小宽高设置好,这个我们下一篇会详细说。
Draw
Draw过程,就是在canvas上画出我们需要的View样式。同样View给我们暴露了onDraw方法
protected void onDraw(Canvas canvas) { }
默认View类的onDraw没有一行代码,但是提供给我们了一张空白的画布,举个例子,就像一张画卷一样,我们就是画家,能画出什么样的效果,完全取决我们。
View中还有三个比较重要的方法
requestLayout
View重新调用一次layout过程。
invalidate
View重新调用一次draw过程
forceLayout
标识View在下一次重绘,需要重新调用layout过程。
自定义属性
整个View的绘制流程我们已经介绍完了,还有一个很重要的知识,自定义控件属性,我们都知道View已经有一些基本的属性,比如layout_width,layout_height,background等,我们往往需要定义自己的属性,那么具体可以这么做。
1.在values文件夹下,打开attrs.xml,其实这个文件名称可以是任意的,写在这里更规范一点,表示里面放的全是view的属性。
2.因为我们下面的实例会用到2个长度,一个颜色值的属性,所以我们这里先创建3个属性。
<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>
那么到底怎么用呢,我们会看一个实例。
实现一个比较简单的Google彩虹进度条。
为了简单起见,这里我只用一种颜色,多种颜色就留给大家了,我们直接上代码。
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); } ....... }
View有了三个构造方法需要我们重写,这里介绍下三个方法会被调用的场景,
第一个方法,一般我们这样使用时会被调用,View view = new View(context);
第二个方法,当我们在xml布局文件中使用View时,会在inflate布局时被调用,
c817341b0684688247af9fe0cab88e38。
第三个方法,跟第二种类似,但是增加style属性设置,这时inflater布局时会调用第三个构造方法。
943770e95a20c49afd54443e38c0041e。
上面大家可能会感觉到有点困惑的是,我把初始化读取自定义属性hspace,vspace,和barcolor的代码写在第三个构造方法里面,但是我RainbowBar在线性布局中没有加style属性(),那按照我们上面的解释,inflate布局时应该会invoke第二个构造方法啊,但是我们在第二个构造方法里面调用了第三个构造方法,this(context, attrs, 0); 所以在第三个构造方法中读取自定义属性,没有问题,这是一点小细节,避免代码冗余-,-
Draw
因为我们这里不用关注measrue和layout过程,直接重写onDraw方法即可。
//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(); }
布局文件:
<?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:gravity="center" android:layout_marginTop="40dp" android:orientation="vertical" > <com.sw.demo.widget.RainbowBar android:layout_width="match_parent" android:layout_height="wrap_content" app:rainbowbar_color="@android:color/holo_blue_bright" app:rainbowbar_hspace="80dp" app:rainbowbar_vspace="10dp" ></com.sw.demo.widget.RainbowBar> </LinearLayout>
其实就是调用canvas的drawLine方法,然后每次将draw的起点向前推进,在方法的结尾,我们调用了invalidate方法,上面我们已经说明了,这个方法会让View重新调用onDraw方法,所以就达到我们的进度条一直在向前绘制的效果。下面是最后的显示效果,制作成gif时好像有色差,但是真实效果是蓝色的。我们只写了短短的几十行代码,自定义View并不是我们想象中那么难,下一篇我们会继续ViewGroup的绘制流程学习。
自定义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中文网!

本文讨论了使用Maven和Gradle进行Java项目管理,构建自动化和依赖性解决方案,以比较其方法和优化策略。

本文使用Maven和Gradle之类的工具讨论了具有适当的版本控制和依赖关系管理的自定义Java库(JAR文件)的创建和使用。

本文讨论了使用咖啡因和Guava缓存在Java中实施多层缓存以提高应用程序性能。它涵盖设置,集成和绩效优势,以及配置和驱逐政策管理最佳PRA

本文讨论了使用JPA进行对象相关映射,并具有高级功能,例如缓存和懒惰加载。它涵盖了设置,实体映射和优化性能的最佳实践,同时突出潜在的陷阱。[159个字符]

Java的类上载涉及使用带有引导,扩展程序和应用程序类负载器的分层系统加载,链接和初始化类。父代授权模型确保首先加载核心类别,从而影响自定义类LOA


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver Mac版
视觉化网页开发工具

WebStorm Mac版
好用的JavaScript开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

mPDF
mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),