前天看了同事的自定义view博客.也对view的绘制也做一个总结. 高手请指教,小白可以借鉴.
为了将相同的布局文件在不同的地方使用,不用ctrl+c,ctrl+v,修改时再到处ctrl+f.所以将这个布局文件抽取出来,然后在需要使用的地方引用.
创建文件: layout_two_button.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_weight="1" android:text="被观察" /> <Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_weight="1" android:text="观察" /> LinearLayout>
在需要的地方引用就可以了
<include layout="@layout/layout_two_button" />
可是这些布局的点击函数大多相同,为了不在java文件中再设置相同的点击事件,
而xml只是布局文件,不是对象是没有点击事件的.继续将这些抽取出来的文件,变成类对象.
public class ButtonLineLayout extends LinearLayout { public ButtonLineLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); View inflate = LayoutInflater.from(context).inflate(R.layout.layout_two_button, this); inflate.findViewById(R.id.button1).setOnClickListener(v -> Toast.makeText(context, "button1", Toast.LENGTH_SHORT).show()); } }
将创建的类对象在需要的文件中引用
<com.ui.material.ButtonLineLayout android:layout_width="match_parent" android:layout_height="wrap_content" />
说到底,这只不过是一个特殊的lineLayout,里面添加了我们视图,并赋予了点击事件而已。
小菜开胃,开始正餐。
首先来认识一个变量MeaureSpec. MeasureSpec 是一个32位的Int值。
MeasureSpec的前2位是表示测量模式,后面30位是视图的大小。
子view MeasureSpec 是由 父布局的 MeasureSpec 和 自身 布局属性 决定的.
测量模式和wrapCont的关系简单的记:
Exactly下的matchParent就是Exactly的.
at_most下的matchParent就是at_most .上天注定的
首先纠正一个误区。textVeiw中的wrap_content可以实现自适应大小,是人家做了代码逻辑处理。
不是把你的view设置了wrap_content也不用管他的大小了。。
它不会孙的铁棒可以你想多大就可以多大。
在源码中view的measure->onMeasure->getDeafultSize方法中可以看到at_most,exactly 没有区别。
哎,不给你看,来打我呀。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); /** * 获取测量模式和大小 */ int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpec = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); /** * 设置默认值 */ if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(23, 23); }else if(widthMode == MeasureSpec.AT_MOST ){ setMeasuredDimension(23,heightSize); }else if(heightMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpec,23); } }
总结 :
MeasureSpec.getMode 获取模式
MeasureSpec.getSize 获取大小
setMeasuredDimension 设置view大小
继承系统控件只是对功能进行扩展,不做过多描述.
下面看如何继承view实现绘制.
public class CirecleView extends View { private int mColor = Color.RED; private Paint mPaint = new Paint(ANTI_ALIAS_FLAG); public CirecleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint.setColor(mColor); /** * 画一个圆 */ int width = getWidth(); int height = getHeight(); int radius = Math.min(width, height) / 2; canvas.drawCircle(width / 2, height / 2, radius, mPaint); } }
public class CirecleView extends View { ****** ~~~~~~ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //由一个是精确模式,就取精确模式的值. if (widthMode == MeasureSpec.EXACTLY || heightMode == MeasureSpec.EXACTLY) { int size = Math.min(widthSize, heightSize); setMeasuredDimension(size, size); } else { //否则使用默认值 int defaultSize = 400; setMeasuredDimension(defaultSize, defaultSize); } } }
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint.setColor(mColor); //首先明确一点,视图的宽高已经固定,padding越大,半径越小 int width = getWidth() - (getPaddingLeft() + getPaddingRight()) ; int height = getHeight() - (getPaddingTop() + getPaddingBottom()) ; int radius = Math.min(width, height) / 2; //然后左,上决定位置 canvas.drawCircle(width / 2 + getPaddingLeft(), height / 2 + getPaddingTop(), radius, mPaint); }
attrs.xml中设置 类,属性,类型
//类 //属性 //类型
public CirecleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); //获取类下所有的属性 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CirecleView); mColor = typedArray.getColor(R.styleable.CirecleView_circle_color, 0xff0000); typedArray.recycle(); }
onMeasure 中添加wrapContent的判断,显示大小
onDraw中添加padding的支持
再提供一些自定义的属性.一个简单的自定义view就完成了
我们可以通过setMeasuredDimension来设置view的大小,但是其他view的大小我们如何得知呢.
getWidth()?getMeasureWidth()?
view的绘制和activity中生命周期没有关系。
onResume时,acitivity都可以可见互动了,你还没绘制好,玩我呢。
通常做法:
view.post(() -> { view.getMeasuredHeight(); });
补充说明:
getMeasuredHeight 是测量大小,
getHight是布局大小。
一般而言两者没有区别,但是可以有,只不过没啥意义. 【ps:我的面试题,当时一脸懵逼】
重写Layout方法后,测量宽度getMeasuredHeight 会比getHight小100.
@Override public void layout(int l, int t, int r, int b) { super.layout(l, t, r+100, b+100); }
如果是matchParent模式:
在绘制view的时候还不知道父布局的大小,所以无解
如果是wrapContent模式
//创建一个MeaureSpec:告知view的最大值和测量模式 int heightSpec = weidthSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,MeasureSpec.AT_MOST) //给爷去测量 view.measure(weidthSpec,heightSpec) //获取测量的数值 view.getMeasuredWidth();
ps:需要准确的告知测量模式
Log.d("test",cire.measuredHeight.toString()) // 0 val widthSpec = MeasureSpec.makeMeasureSpec(resources.getDimension(R.dimen.fab_margin_50).toInt(), MeasureSpec.EXACTLY) val heightSpec= MeasureSpec.makeMeasureSpec(Int.MAX_VALUE shr 2, MeasureSpec.AT_MOST) cire.measure(widthSpec,heightSpec) Log.d("test",cire.measuredHeight.toString()) // 正确结果131 Log.d("test",cire.height.toString()) // 0
结果发现getHeight测量后还是0,getMeasureHeight就已经有值了
上面刚说getMeasuredHeight 和 getHight没有区别 ,结果就啪啪的打脸.我不要面子的嘛.-.-
就画一个圈,就说会绘制view了.你在敷衍谁呢,好歹告诉我怎么画一个框呀.好满足你.api大法上!
借鉴T9的第三个三角的博客,在这里表示感谢.
/** * Canvas.drawXXX() 方法,都是以左上角作为基准点的,而 drawText() 却是文字左下方 * Canvas.drawText() 只能绘制单行的文字,而不能换行 * 不能在换行符 n 处换行 * 需要绘制多行的文字可以使用StaticLayout */ mPaint.setColor(Color.GRAY); mPaint.setStyle(Paint.Style.STROKE); mPaint.setTypeface(Typeface.SANS_SERIF);//字体 mPaint.setFakeBoldText(true);//粗体 mPaint.setStrikeThruText(true);//删除线 mPaint.setUnderlineText(true); //下划线 mPaint.setTextSkewX(-0.4f);//倾斜 // mPaint.setLetterSpacing(0.8f);//字符间距 // mPaint.setTextAlign(Paint.Align.RIGHT);//对齐 mPaint.setStrokeWidth(5); mPaint.setTextSize(200); canvas.drawText("practice",20,200,mPaint); /** * 测量text的范围 */ Rect bounds = new Rect(); mPaint.getTextBounds("practice",0,"practice".length(),bounds); bounds.left += 20; bounds.right += 20; bounds.top += 200; bounds.bottom += 200; canvas.drawRect(bounds,mPaint); /** * 测量text的宽度 */ float textWidth = mPaint.measureText("practice"); // bounds.width(); canvas.drawLine(20,200, textWidth += 20,200,mPaint); /** * 测量高度 */ Paint.FontMetrics fontMetrics = mPaint.getFontMetrics(); float height1 = fontMetrics.descent - fontMetrics.ascent; float height2 = fontMetrics.bottom - fontMetrics.top; /** * 画布裁剪和绘制图片 */ canvas.clipRect(100,100,400,400); Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_action_name); canvas.drawBitmap(bitmap,100,100,mPaint); /** * 按路径裁剪 */ Path path = new Path(); path.addCircle(500,500,400, Path.Direction.CCW); canvas.clipPath(path); /** * 平移 */ canvas.translate(300,100); /** * 旋转 图层 */ canvas.rotate(200); /** * 缩放 */ canvas.scale(1.5f,0.8f,100,100); /** * 画圈 */ canvas.drawCircle(20, 20, 4, mPaint); /** * 画矩形 */ canvas.drawRect(20,0,20,90,mPaint);