一、 什么是View
(1)View 是 Android 中所有控件的基类
不管是简单的控件 TextView、Button,还是复杂的 RecyclerView、LinearLayout 等,它们的共同基类是 View。
(2)ViewGroup 也继承自 View
View 本身可以是单个控件,也可以是一组控件,Android 中的 UI 组件都由 View 和 ViewGroup 组成。
二、 View的位置参数
View 的位置主要由它的四个顶点确定,再简单来说,根据左上角、右下角这两个相对的顶点即可确定 View 的位置。top 是左上角的纵坐标、left 是左上角的横坐标、right 是右下角的横坐标、bottom 是右下角的纵坐标。
上述4个属性和 View 宽/高的关系是:
width=right-left
height=bottom-top
三、 获取 View 的位置参数的方式
(1)top、left、right、bottom
在 View 类的源码中上述四个属性分别存储在以下变量中:mLeft
、mTop
、mRight
、mBottom
,分别提供了对应的get
方法。这四个方法获取的是 View 的原始状态相对于父容器的坐标,对 View 进行平移不会改变上述方法返回值。
left=getLeft();
protected int mLeft;
public final int getLeft() {
return mLeft;
}
top=getTop();
right=getRight();
bottom=getBottom();
示例如下:
public class TestActivity extends AppCompatActivity {
private static final String TAG = "TestActivity";
private View childView;
private View parentView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
childView = findViewById(R.id.child_view);
parentView = findViewById(R.id.parent_view_ll);
initListenter();
}
private void initListenter() {
parentView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onGlobalLayout() {
parentView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
parentView.getTop();
Log.d(TAG, String.format("parent::--->top=%d;bottom=%d;left=%d;right=%d", parentView.getTop(), parentView.getBottom(), parentView.getLeft(), parentView.getRight()));
}
});
childView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onGlobalLayout() {
childView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
childView.getTop();
Log.d(TAG, String.format("child::--->top=%d;bottom=%d;left=%d;right=%d", childView.getTop(), childView.getBottom(), childView.getLeft(), childView.getRight()));
}
});
}
打印结果
2020-07-26 17:05:41.137 8126-8126/? D/TestActivity: parent::--->top=873;bottom=1173;left=390;right=690
2020-07-26 17:05:41.138 8126-8126/? D/TestActivity: child::--->top=100;bottom=200;left=100;right=200
说明top、bottom、left、right:都是以父控件坐标系为参考的坐标
(2)x、y、translationX、translationY
Android 3.0 开始,View 类中增加了额外的几个参数:x、y、translationX、translationY,并提供了相应的方法获取相应的值。
- x、y
getX()
、getY()
方法获取的是 View 左上角相对于父容器坐标系的坐标,当 View 未发生平移时,getX()=getLeft()
、getY()=getTop()
public float getX() {
return mLeft + getTranslationX();
}
- translationX、translationY
getTranslationX()
、getTranslationY()
方法获取的是 View 左上角相对于父容器的平移量。
public float getTranslationX() {
return mRenderNode.getTranslationX();
}
x、y、translationX、translationY的换算关系是
x=left+translationX
、y=top+translationY
(3)MotionEvent 中获取用户点击点位置坐标
可以重写 View 类中的onTouchEvent()
方法
@Override
public boolean onTouchEvent(MotionEvent event) {
event.getX();
event.getY();
event.getRawX();
event.getRawY();
}
在方法中可以调用MotionEvent
的getX()
、getRawX()
方法获取位置信息,二者的区别如下。
getX()
得到触摸点相对于其所在组件坐标系的坐标getRawX()
得到触摸点相对于所在屏幕坐标系的坐标
也可以为 View 设置onTouchListener()
,在其中获取 MotionEvent 的位置坐标 自定义View(一)、基础概念和知识点
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, String.format("x=%f;y=%f;rawX=%f;rawY=%f", event.getX(), event.getY(), event.getRawX(), event.getRawY()));
return false;
}
});
示例如下:
public class TestActivity extends AppCompatActivity {
private static final String TAG = "TestActivity";
private View childView;
private View parentView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
childView = findViewById(R.id.child_view);
parentView = findViewById(R.id.parent_view_ll);
initListenter();
}
private void initListenter() {
childView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// 触摸child-view的左上角那一点
Log.d(TAG, String.format("x=%f;y=%f;rawX=%f;rawY=%f", event.getX(), event.getY(), event.getRawX(), event.getRawY()));
// getX()、getY():以View自身坐标系为参考的坐标
// getRawX()、getRawY():以屏幕坐标系为参考的坐标
return false;
}
});
}
}
四、 Android 中颜色相关内容
(1)Android 中的颜色模式
英文表示通道类型,数字表示用多少个二进制位表示对应通道。
(2)定义颜色的方式
- 在 java 文件中
int color=Color.GRAY;
int color=Color.argb(127,255,0,0);
int color=0xaaff0000;// 高精度带透明度
- 在 xml 文件(资源文件)中
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
</resources>
其中颜色的写法有四种:高精度带透明度#aaff0000
、高精度不带透明度#ff0000
、低精度带透明度#af00
、低精度不带透明度#f00
。
(3)引用颜色的方式
- 在 java 文件中
API≥23
int color=getColor(R.Color.Red);
API<23
int color=getResources().getColor(R.Color.Red);
- 在 xml 文件中
layout 文件中引用/res/values/color.xml
文件中内容
android:background="@color/colorPrimary"
layout 文件中创建并引用颜色
android:background="#ff0000"
style 文件中引用/res/values/color.xml
文件中内容
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/red</item>
</style>
style 文件中创建并引用颜色
android:background="#ff0000"
五、 Android 中的 style 和 theme
自定义 View——Android 中的 style 和 theme
六、View 的构造函数
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
Q:View 类有4个构造函数,每个参数的含义?
- Context context
Q:继承 View 类自定义控件时需要实现哪几个构造函数?
七、 ViewGroup.LayoutParams
自定义控件知识储备-LayoutParams的那些事
自定义控件知识储备-View的绘制流程
1. 是什么
顾名思义,布局参数。布局文件 XML 中,以layout_
开头的属性都和LayoutParams
有关。
2. 作用
子 View 通过布局参数告诉父控件自己想要被如何放置
LayoutParams 基类(ViewGroup.LayoutParams,ViewGroup 的静态内部类)仅描述了 View 想要的宽度和高度
不同的 ViewGroup 继承类对应不同的 ViewGroup.LayoutParams 子类
如:LinearLayout,LinearLayout.LayoutParams;RelativeLayout,RelativeLayout.LayoutParams
3. 使用
(1)ViewGroup.MarginLayoutParams
(2)在 XML 布局文件里使用 LayoutParams 中的各种属性
LayoutParams 常用在 XML 布局文件里,使用容器控件的 LayoutParams 中的各种属性来给孩子们布局。这种方式直观方便,可直接在预览界面中看到效果,但同时界面也被写死了,无法动态改变。
(3)在代码中为父容器动态地添加子 View
下面是几种常见的添加 View 的方式。通过调用父控件的addView()
完成添加。
LinearLayout parent = (LinearLayout) findViewById(R.id.ll_content);
// 1,直接添加一个 TextView,不主动指定LayoutParams
TextView textView1 = new TextView(this);
textView1.setText("哈哈");
textView1.setTextColor(Color.RED);
parent.addView(textView1);
// 2,先手动为 TextView 指定 LayoutParams,再添加
TextView textView2 = new TextView(this);
textView2.setText("哈哈");
textView2.setTextColor(Color.RED);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(300, 300);
textView2.setLayoutParams(lp);
parent.addView(textView2);
// 3,向容器中添加子View时,指定LayoutParams
TextView textView3 = new TextView(this);
textView3.setText("哈哈");
textView3.setTextColor(Color.RED);
LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(300, 300);
parent.addView(textView3, lp2);
LinearLayout
、RelativeLayout
等容器的addView()
方法都是继承自ViewGroup
类,该类中提供了4个addView()
方法的重载。
- ① addView(View child)
public void addView(View child) {
addView(child, -1);
}
- ② addView(View child, int index)
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// 得到子View自身设置的LayoutParams
LayoutParams params = child.getLayoutParams();
if (params == null) {
// 若未设置,则调用generateDefaultLayoutParams()得到默认的布局参数
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
- ③ addView(View child, int index, LayoutParams params)
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
- ④ addView(View child, int width, int height)
public void addView(View child, int width, int height) {
final LayoutParams params = generateDefaultLayoutParams();
params.width = width;
params.height = height;
addView(child, -1, params);
}
上述方法中调用的其他相关方法。
- ① generateDefaultLayoutParams()
ViewGroup 类中的generateDefaultLayoutParams()。
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
在 LinearLayout 中重写了generateDefaultLayoutParams()方法。
@Override
protected LayoutParams generateDefaultLayoutParams() {
if (mOrientation == HORIZONTAL) {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
} else if (mOrientation == VERTICAL) {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
return null;
}
这个方法的实现告诉我们,很有可能因为我们的懒,导致布局效果和我们理想的不一样。因此第一种添加 View 的方式不推荐,在添加 View 的时候我们应该手动指定 LayoutParams,这样不仅明确,而且易修改。
(3)自定义 LayoutParams
每个容器控件几乎都会有自己的 LayoutParams 实现,像 LinearLayout、RelativeLayout等。所有我们自定义 ViewGroup 时,几乎都要自定义相应的 LayoutParams。
- LinearLayout.LayoutParams
先看下线性布局中布局参数的定义。
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
/**
* Indicates how much of the extra space in the LinearLayout will be
* allocated to the view associated with these LayoutParams. Specify
* 0 if the view should not be stretched. Otherwise the extra pixels
* will be pro-rated among all views whose weight is greater than 0.
*/
@ViewDebug.ExportedProperty(category = "layout")
public float weight;
public int gravity = -1;
/**
* {@inheritDoc}
*/
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a =
c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);
a.recycle();
}
/**
* {@inheritDoc}
*/
public LayoutParams(int width, int height) {
super(width, height);
weight = 0;
}
/**
* Creates a new set of layout parameters with the specified width, height
* and weight.
*
* @param width the width, either {@link #MATCH_PARENT},
* {@link #WRAP_CONTENT} or a fixed size in pixels
* @param height the height, either {@link #MATCH_PARENT},
* {@link #WRAP_CONTENT} or a fixed size in pixels
* @param weight the weight
*/
public LayoutParams(int width, int height, float weight) {
super(width, height);
this.weight = weight;
}
/**
* {@inheritDoc}
*/
public LayoutParams(ViewGroup.LayoutParams p) {
super(p);
}
/**
* {@inheritDoc}
*/
public LayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
}
/**
* Copy constructor. Clones the width, height, margin values, weight,
* and gravity of the source.
*
* @param source The layout params to copy from.
*/
public LayoutParams(LayoutParams source) {
super(source);
this.weight = source.weight;
this.gravity = source.gravity;
}
@Override
public String debug(String output) {
return output + "LinearLayout.LayoutParams={width=" + sizeToString(width) +
", height=" + sizeToString(height) + " weight=" + weight + "}";
}
/** @hide */
@Override
protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
super.encodeProperties(encoder);
encoder.addProperty("layout:weight", weight);
encoder.addProperty("layout:gravity", gravity);
}
}
- 自定义 LayoutParams
参考文献
自定义View基础 - 最易懂的自定义View原理系列(1)
View 构造函数
深入理解Android View的构造函数