4个顶点的位置描述分别由4个值决定:
(请记住:View的位置是相对于父控件而言的)
Top:子View上边界到父view上边界的距离
Left:子View左边界到父view左边界的距离
Bottom:子View下边距到父View上边界的距离
Right:子View右边界到父view左边界的距离
LayoutInflate主要用于加载布局,包括在Activity中调用setContentView(),方法内部其实也是用LayoutInflate来实现的。
//两种初始化方式
LayoutInflater inflater = LayoutInflater.from(this);
// LayoutInflater inflater1 = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
因为在源码里,from就是一个系统封装好的方法,里面用了context.getSystemService……
inflater.inflate(resourceId, root);
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main_layout"
android:orientation="vertical">
</LinearLayout>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="button">
</RelativeLayout>
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LinearLayout mainLayout = findViewById(R.id.main_layout);
//两种初始化方式
LayoutInflater inflater = LayoutInflater.from(this);
// LayoutInflater inflater1 = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View buttonLayout = inflater.inflate(R.layout.button_layout, null);
mainLayout.addView(buttonLayout);
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="300dp"
android:layout_height="100dp"
android:text="button">
</RelativeLayout>
发现button大小并未改变。
layout_width和layout_height 其实是用于设置View在布局中的大小的,也就是View必须存在于一个布局中,这两个参数的设定才有效。这也是为什么这两个属性叫作layout_width和layout_height,而不是width和height。
所以最简单的是在Button的外面再套一个布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:layout_width="300dp"
android:layout_height="150dp"
android:text="button"/>
</RelativeLayout>
ViewParent parent = mainLayout.getParent();
Log.d("MainActivity", "the parent of mainLayout is" + parent);
log信息
可以看到,LinearLayout的父布局确实是一个FrameLayout,而这个FrameLayout就是由系统自动帮我们添加上的。
组合控件的意思就是,我们并不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,我们将几个系统原生的控件组合到一起,例如最常见的 标题栏。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<Button
android:id="@+id/btn_back"
android:layout_width="100dp"
android:layout_height="50dp"
android:text="back"
android:textAllCaps="false"/>
<TextView
android:id="@+id/tv_title"
android:layout_width="250dp"
android:layout_height="50dp"
android:text="This is title"
android:textAllCaps="false"
android:gravity="center_horizontal"/>
<ImageView
android:id="@+id/iv_home"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@mipmap/user"/>
</LinearLayout>
刚开始我是这么写的:
在public TitleView(Context context) {} 的构造方法中,调用LayoutInflater的inflate()方法来加载刚刚定义的title.xml布局,并初始化控件。
用setTitleText方法设置标题文字,用setBackListener方法设置返回键点击响应等等……
public class TitleView extends FrameLayout implements View.OnClickListener {
private Button mBtnBack;
private TextView mTvTitle;
private ImageView mIvHome;
public TitleView(Context context) {
super(context);
LayoutInflater.from(context).inflate(R.layout.view_title, this);
mBtnBack = findViewById(R.id.btn_back);
mTvTitle = findViewById(R.id.tv_title);
mIvHome = findViewById(R.id.iv_home);
mBtnBack.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_back:
((Activity)getContext()).finish(); //结束当前activity
break;
}
}
public void setTitleText(String text) {
mTvTitle.setText(text);
}
public void setBackText(String text) {
mBtnBack.setText(text);
}
public void setBackListener(OnClickListener listener) {
mBtnBack.setOnClickListener(listener);
}
public void setImageListener(OnClickListener listener) {
mIvHome.setOnClickListener(listener);
}
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main_layout"
android:orientation="vertical">
<com.sky.customapplication.TitleView
android:id="@+id/title_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.sky.customapplication.TitleView>
</LinearLayout>
public class MainActivity extends AppCompatActivity {
private TitleView mTitleView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTitleView = findViewById(R.id.title_view);
mTitleView.setBackListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "touched back!", Toast.LENGTH_SHORT).show();
}
});
mTitleView.setImageListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "touched home!", Toast.LENGTH_SHORT).show();
}
});
mTitleView.setBackText("返回去");
mTitleView.setTitleText("我是标题");
}
}
这样,自定义ViewGroup的流程就结束了!让我们点击运行……
什么(O_o)???居然报错了??
嗯??我的TitleView加载失败了?为啥哟
没办法,一顿查资料问师父……
原来问题出在FrameLayout的构造方法上,让我们再回过头看一下,还原案发现场……
当时的情况是这样的:
那我不就alt+enter,创建构造函数
然后选了默认的第一个…问题就出在这里!!
一般来说,需要写前三个构造函数。那么问题来了,init的内容写在哪里呢?
通常情况下,应该这么写
//java代码中new的时候调用
public TitleView(Context context) {
this(context, null);
}
// xml中引用时调用
public TitleView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
//theme、style时调用
public TitleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater.from(context).inflate(R.layout.view_title, this);
mBtnBack = findViewById(R.id.btn_back);
mTvTitle = findViewById(R.id.tv_title);
mIvHome = findViewById(R.id.iv_home);
mBtnBack.setOnClickListener(this);
}
实现这样一个控件,每点击一次显示的数字加1;控件大小适应数字大小,周围可以设置padding。
参考鸿洋大神的Android 深入理解Android中的自定义属性
有以下几个步骤:
Sdk/platforms/android-xx/data/res/values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="textColor" format="color"/>
<attr name="textSize" format="dimension"/>
<declare-styleable name="title_attrs">
<!--尺寸值,格式是dimension-->
<attr name="width" format="dimension"/>
<attr name="height" format="dimension"/>
<attr name="textColor"/>
<attr name="textSize"/>
<!--声明需要使用系统定义过的text属性,注意前面需要加上android命名-->
<attr name="android:text"/>
</declare-styleable>
<declare-styleable name="RectangleView">
<attr name="textColor"/>
<attr name="textSize"/>
<attr name="android:text"/>
</declare-styleable>
</resources>
xmlns:title="http://schemas.android.com/apk/res-auto"
title:
里获取,一般用app:
) <com.sky.customapplication.TitleView
android:id="@+id/title_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
title:textColor="#000"
title:textSize="26sp"
android:text="im text">
</com.sky.customapplication.TitleView>
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.RectangleView, defStyleAttr, 0);
//mText = array.getString(R.styleable.RectangleView_android_text);
mTextColor = array.getColor(R.styleable.RectangleView_textColor, Color.BLACK);
mTextSize = array.getDimensionPixelSize(R.styleable.RectangleView_textSize, 40);
array.recycle(); //注意回收
public class RectangleView extends View implements View.OnClickListener {
private Paint mPaint;
private Rect mBounds;
//private String mText;
private float mTextSize;
private int mTextColor;
private int mCount;
private String text;
public RectangleView(Context context) {
this(context, null);
}
public RectangleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RectangleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//新建画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); //抗锯齿
mBounds = new Rect();
//加载自定义属性集合
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.RectangleView, defStyleAttr, 0);
// 将解析的属性传入到画笔颜色变量当中(本质上是自定义画笔的颜色)
// 第二个参数是默认设置颜色(即无指定color情况下使用)
//mText = array.getString(R.styleable.RectangleView_android_text);
mTextColor = array.getColor(R.styleable.RectangleView_textColor, Color.BLACK);
mTextSize = array.getDimensionPixelSize(R.styleable.RectangleView_textSize, 40);
array.recycle(); //记得回收
setOnClickListener(this);
}
@Override
protected void onDraw(Canvas canvas) {
//画笔颜色
mPaint.setColor(Color.YELLOW);
//画一个长方形
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
//设置画字的颜色
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize);
text = String.valueOf(mCount);
mPaint.getTextBounds(text, 0, text.length(), mBounds);
float textWidth = mBounds.width();
float textHeight = mBounds.height();
//文字绘制的起点是从文字的左下角开始的,实际看见文字的Y坐标需要加上文字的自身高度
canvas.drawText(text, getWidth()/2 - textWidth/2, getHeight()/2 + textHeight/2, mPaint);
}
@Override
public void onClick(View v) {
mCount++;
invalidate(); //视图重绘,onDraw调用
}
<com.sky.customapplication.RectangleView
android:layout_width="200dp"
android:layout_height="100dp"
title:textColor="#00FF33"
title:textSize="30sp"
android:padding="15dp"
/>
重写之前先了解MeasureSpec的specMode,一共三种类型:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
mPaint.setTextSize(mTextSize);
text = String.valueOf(mCount);
mPaint.getTextBounds(text, 0, text.length(), mBounds);
float textWidth = mBounds.width();
width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
}
if (heightMode == MeasureSpec.EXACTLY)
{
height = heightSize;
} else
{
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(text, 0, text.length(), mBounds);
float textHeight = mBounds.height();
height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
}
setMeasuredDimension(width, height);
}
padding属性在自定义View中默认也是无效的,如果不重写onMeasure,也需要能够设置padding,可以在onDraw中这样写:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取传入的padding值(指定padding,否则设置无效)
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
//获取控件的宽高
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
//设置半径为宽、高最小值的1/2
int r = Math.min(width, height)/2;
//画圆:圆心位置为控件中央,半径,画笔
canvas.drawCircle(paddingLeft + width/2, paddingTop + height/2, r, mPaint);
}
padding属性:用于设置控件内容相对控件边缘的边距;
区别与margin属性(同样称为:边距):控件边缘相对父控件的边距(父控件控制),具体区别如下:
其中,background属性可以直接用android:backgroud来设置,不需要自定义属性
待学:https://www.jianshu.com/p/0599a90d125a
https://www.jianshu.com/p/9759a1666494
https://www.imooc.com/learn/793
https://www.imooc.com/video/7442
https://blog.csdn.net/xmxkf/article/details/51490283#1_onMeasure_15
https://www.jianshu.com/p/afa06f716ca6
1. canvas api
2. LayoutParams