自定义 View —— 知识准备
目录
2.1 由于 测量模式,引起的 Scrollview 嵌套 ListView 显示不全的问题(源码)
一、为什么要自定义 View?
当 Android 系统内置的 View 无法实现我们的需求,我们就需要根据需求写一个想要的 View。
二、自定义 view 构造函数的调用
/**
* TextView textView = new TextView(this);
* 会在代码中 new 的时候调用
*/
public TextView(Context context) {
super(context);
}
/**
<com.yuan.customview.day01.TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="111"/>
* 在布局 layout 中使用
*/
public TextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* <com.yuan.customview.day01.TextView
* style="@style/TextView_Default"
* android:text="111"/>
*
* 在布局 layout 中使用,但是会有 style,多个布局中,有相同 style 时,写一个 style 是一个
* 很好的方式
*/
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
二、onMeasure 方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 布局的宽高 都是由这个方法指定
// 指定控件的宽高,需要测量
// 获取宽高的测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getMode(heightMeasureSpec);
/**
* 3 种模式
* MeasureSpec.AT_MOST:在布局中指定了 wrap_content
* MeasureSpec.EXACTLY:在布局中指定确切的值 150dp ,match_parent
* MeasureSpec.UNSPECIFIED:尽可能的大 很少会用到 ListView,ScrollView 在测量子布局
* 的时候会调用 UNSPECIFIED
*/
}
2.1 由于 测量模式,引起的 Scrollview 嵌套 ListView 显示不全的问题(源码)
首先,我们看一下 ScrollView 的 onMeasure 方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 继承父类的方法
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!mFillViewport) {
return;
}
.......
}
接着,我们看父类的 onMeasure 方法,也就是 FrameLayout 的 onMeasure 方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
......
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 循环查询自己的子 View,如果子 View 不是 GONE,就执行下面这个方法
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
而 measureChildWithMargins 方法,ScrollView 中重写了该方法,接下来,我们看一下 ScrollView 的 measureChildWithMargins 方法
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
// 在这里,你会发现,对 childHeightMeasureSpec 进行了设值,设置他的高度的测量模式
// heightMode 为 MeasureSpec.UNSPECIFIED 模式
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
// 紧接着就进入了子 View 的OnMeasure 方法,也就是 ListView 的 onMeasure 方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
接着看 ListView 的 onmeasure 方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 这里 通过 MeasureSpec.getMode() 将 ScrollView 传过来的heightMode 获取到
// 也就是 MeasureSpec.UNSPECIFIED 模式
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 省略一些代码,看我们想看的代码
......
// 之后就会进入 这个 if 判断
if (heightMode == MeasureSpec.UNSPECIFIED) {
// 这里获取到 ListView 的高度 = top + bottom + 一个 子view 的高度
// 也就是导致了 ListView 只会显示一个 item 高度
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
// ------ 其实我们需要的是进入这个 if 判断中
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
// measureHeightOfChildren 是循环获取 item 的高度,并相加
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
// 获取完宽、高后,进行设置
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
所以,就有了解决方案:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 重新修改 ListView 的 heightMode
heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
解释一下为什么这么写:
MeasureSpec.makeMeasureSpec(int size,int mode)
第二个参数测量模式 修改为 MeasureSpec.AT_MOST ,应该好理解,因为我们需要进入 ListView 的 onmeasure 方法中
if (heightMode == MeasureSpec.AT_MOST) 这个判断中。
第一个参数为什么是 Integer.MAX_VALUE >> 2?
先说一下是什么意思,就是 将 Integer.MAX_VALUE 右移两位。
再说一下 MeasureSpec,MeasureSpec 其实是一个 32 位的 int 值,高两位代表 SpecMode,也就是测量模式,低 30 位代表 SpecSize 也就是测量的值。
而在 MeasureSpec.makeMeasureSpec(int size,int mode) 这个方法中,我们已经指定了 高两位 的 mode,剩下的就是值的大小,Integer.MAX_VALUE 表示的是一个 32 位的值,所以我们需要将它右移两位,变成 30 位的值,这样 + 高两位的mode 就等于 32位了。
举个简单的例子吧:简单可以理解为 32 - 2 = 30
三、onDraw 方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 用于绘制
// 画文本
canvas.drawText();
// 画弧
canvas.drawArc();
// 画圆
canvas.drawCircle();
// .....等等
}
四、onTouchEvent 方法
/**
* 处理与用户交互的事件,手指触摸等等
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
// 手指按下
break;
case MotionEvent.ACTION_MOVE:
// 手指滑动
break;
case MotionEvent.ACTION_UP:
// 手指抬起
break;
}
return super.onTouchEvent(event);
}
五、自定义属性
1、在res 下的 values 中创建 attrs.xml 资源文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 自定义属性,就是用来配置自定义 View 的 -->
<!-- 自定义属性的 name 最好就是 自定义 View 的名字 -->
<declare-styleable name="TextView">
<!-- name 属性名称 ,format 格式 -->
<!-- 格式: string 文字,color 颜色,dimension 宽高、字体大小,
integer 数字,reference 资源(drawable) -->
<attr name="CustomText" format="string"/>
<attr name="CustomColor" format="color"/>
<attr name="CustomTextSize" format="dimension"/>
<attr name="CustomMaxLength" format="integer"/>
<!-- 枚举 -->
<attr name="inputType">
<enum name="number" value="1"/>
<enum name="text" value="2"/>
<enum name="password" value="3"/>
</attr>
</declare-styleable>
</resources>
2、在 layout 布局中使用(先在 layout 中声明命名空间,然后在自己的自定义 View 中使用)
命名空间:xmlns:app="http://schemas.android.com/apk/res-auto"
<com.yuan.customview.day01.TextView
style="@style/TextView_Default"
app:CustomText="111"
app:CustomColor="@android:color/holo_blue_bright"/>
3、在自己写的自定义 View 中获取属性
public class TextView extends View {
private String mText;
private int mTextSize = 14;
private int mTextColor = Color.BLACK;
public TextView(Context context) {
this(context, null);
}
public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
// 使用 this,最终调用这个构造方法
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 获取自定义属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TextView);
mText = array.getString( R.styleable.TextView_CustomText);
mTextSize = array.getDimensionPixelSize(R.styleable.TextView_CustomTextSize, mTextSize);
mTextColor = array.getColor(R.styleable.TextView_CustomColor, mTextColor);
// 记得先回收
array.recycle();
}
}