【Android 笔记】自定义 ViewGroup
ViewGroup 的存在是为了管理其内部子View的测量、显示和事件响应等。自定义 ViewGroup 通常需要重写:
一个可以滑动的 ScrollView,其子控件是垂直方向的线性布局。支持 wrap_content 属性,支持 margin 属性。
准备
- View 的三种测量模式:EXACTLY、AT_MOST 和 UNSPECIFIED
- scrollTo(x,y) 是滚动到坐标点为 x, y 的位置;scrollBy(x,y) 是滚动到 +x, +y 的位置
步骤
1)重写 generateLayoutParams(),以便支持 margin
2)重写 onMeasure(),测量所有子 View,以及自身大小
3)重写 onLayout(),确定子 View 位置
4)重写 onTouchEvent(),响应滚动
代码:
public class MyScrollView extends ViewGroup {
private int mScreenHeight; //屏幕高度
private int mChildsHeight; //wrap_ceontent情况下viewGroup高度(所有子view的高度之和)
private int mChildsWidth; //wrap_ceontent情况下viewGroup宽度(最大子View的宽度)
private int MEASURE_WIDTH = 0; //表示测量宽度
private int MEASURE_HEIGHT = 1; //表示测量高度
private int mLastY; //最后接触的Y坐标
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
mScreenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
Log.v("_v", "create MyScrollView");
}
/**
* 测量子 View
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
mChildsWidth = 0;
mChildsHeight = 0;
//测量所有的子View
measureChildren(widthMeasureSpec, heightMeasureSpec);
//这里计算是为了属性值为wrap_content的情况好给viewGroup赋具体的高宽值
MarginLayoutParams mlp;
for(int i=0; i<count; i++) {
View child = getChildAt(i);
mlp = (MarginLayoutParams) child.getLayoutParams();
//getHeight和getWidth在onLayout执行完之后才能获取
mChildsHeight += child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;
mChildsWidth = Math.max(mChildsWidth, child.getMeasuredWidth() + mlp.leftMargin + mlp.rightMargin); //最大值为viewGroup的宽
}
//设置viewGroup自身高宽
setMeasuredDimension(measureSize(widthMeasureSpec, MEASURE_WIDTH),
measureSize(heightMeasureSpec, MEASURE_HEIGHT));
}
/**
* 测量 viewGroup 的宽或高
*/
private int measureSize(int measureSpec, int type) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec); //获得测量模式
int specSize = MeasureSpec.getSize(measureSpec); //获得测量值
int WRAP_CONTENT;
if(type == MEASURE_WIDTH) {
WRAP_CONTENT = mChildsWidth;
} else {
WRAP_CONTENT = mChildsHeight;
}
switch (specMode) {
case MeasureSpec.EXACTLY:
//精确值模式
result = specSize;
break;
case MeasureSpec.AT_MOST:
//最大值模式
result = WRAP_CONTENT;
break;
case MeasureSpec.UNSPECIFIED:
//未指定模式,未指定我这里当成是at_most
result = WRAP_CONTENT;
break;
}
return result;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//参数changed表示view有新的尺寸或位置;
// 参数l表示相对于父view的Left位置;后同理
int count = getChildCount();
int paintL = 0;
int paintT = 0;
int paintR = 0;
int paintB = 0;
MarginLayoutParams mlp;
for(int i=0; i<count; i++) {
View child = getChildAt(i);
mlp = (MarginLayoutParams) child.getLayoutParams();
//计算子View绘制位置
paintT += mlp.topMargin;
paintL = mlp.leftMargin;
paintR = paintL + child.getMeasuredWidth();
paintB = paintT + child.getMeasuredHeight();
if(child.getVisibility() != GONE) {
child.layout(paintL, paintT, paintR, paintB);
paintT = paintB + mlp.bottomMargin;
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY(); //获得接触到的纵坐标
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int dy = y - mLastY;
//getScrollY为 手机屏幕左上角Y坐标-viewGroup左上角Y坐标
if(getScrollY() < 0) {
dy = 0;
}
if(getScrollY() > getHeight() - mScreenHeight) {
dy = getHeight() - mScreenHeight;
}
//如果越界则滚回
if(dy == 0 || dy == getHeight() - mScreenHeight) {
scrollTo(0, dy);
} else {
//滚动到距离当前位置为dy的位置
//滚动坐标和android坐标相反
scrollBy(0, -dy);
}
mLastY = y;
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_CANCEL:
break;
}
return true;
}
/**
* 为了支持margin,重写此方法返回marginLayoutParams
* @param attrs
* @return
*/
@Override
public MarginLayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
}
xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context="cn.vecrates.androidjinjie.MainActivity">
<cn.vecrates.androidjinjie.MyScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorAccent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="这是一个子 View0"
android:background="#ddc"
android:textSize="20sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="400dp"
android:text="这是一个子 View1"
android:background="#54f"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_marginLeft="40dp"
android:textSize="20sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="400dp"
android:text="这是一个子 View2"
android:background="#ead"
android:textSize="20sp"/>
</cn.vecrates.androidjinjie.MyScrollView>
</LinearLayout>
补充: