前面两篇我们已经相信介绍了 View 以及 ViewGroup 的工作原理,下面我们来说下自定义 View 的一些注意事项吧。
主要介绍内容:
- 自定义View
- 自定义 View 的分类
- 自定义 View 条件条件
- 自定义 View 示例
- 自定义 View 的思想
自定义 View 的分类
自定义 View 的分类标准不唯一,大致可以分为 4 类
- 1、继承 View 重写 onDraw 方法
这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写 onDraw 方法。采用这种方式需要自己支持 wrap_content,并且 padding 也需要自己处理。
- 2、继承 ViewGroup 派生特殊的 Layout
这种方法主要用于实现自定义的布局,即除了 LinearLayout、RelativeLayout、FrameLayout 这几种系统的布局之外,我们重新定义一种新布局,当某种效果看起来很像几种 View 组合在一起的时候,可以采用这种方法来实现。采用这种方式稍微复杂一些,需要合适地处理 ViewGroup 的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
- 3、继承特定的 View (比如 TextView)
这种方法比较常见,一般是用于扩展某种已有的 View 的功能,比如 TextView,这种方法比较容易实现。这种方法不需要自己支持 wrap_content 和 padding 等。
- 4、继承特定的 ViewGroup (比如 LinearLayout)
这种方法也比较常见,当某种效果看起来很像几种 View 组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理 ViewGroup 的测量和布局这两个过程。需要注意这种方法和方法 2 的区别,一般来说 方法 2 能实现的效果方法 4 也都能实现,两者的主要差别在于 方法 2 更接近 View 的底层。
上面介绍了自定义 View 的 4 种方式,读者可以仔细体会一下,是不是的确可以这么划分?但是这里要说的是,自定义 View 讲究的是灵活性,一种效果可能多种方法都可以实现,我们需要做的就是找到一种代价最小、最高效的方法去实现,在下面我们会列举一下自定义 View 过程中常见的注意事项。
自定义 View 常见注意事项
这里我们会列举一些自定义 View 过程中的一些注意事项,这些问题如果处理不好,有些会影响 View 的正常使用,而有些会导致内存泄漏等。
- 1、让 View 支持 wrap_content
这是因为直接继承 View 或者 ViewGroup 的控件,如果不在 onMeasure 中对 wrap_content 做特殊处理,那么外界在布局中使用 wrap_content 时就无法达到预期的效果,具体情形我们已经在前面两篇博客中进行了详细的介绍,这里就不在过多的介绍了。Android——View的工作原理(一)
- 2、如果有必要,让你的 View 支持 padding
这是因为直接继承 View 的控件,如果不在 draw 方法中处理 padding,那么 padding 属性是无法起作用的。另外,直接继承自 ViewGroup 的控件需要在 onMeasure 和 onLayout 中考虑 padding 和 子元素的 margin 对其造成的影响,不然将导致 padding 和 子元素的 margin 失效。
- 3、尽量不要在 View 中使用 Handler,没必要
这是因为 View 内部本身就提供了 post 系列方法,完全可以替代 Handler 的作用,当然除非你很明确地要使用 Handler 来发送消息。
- 4、View 中如果有线程或动画,需要及时停止,参考 View#onDetachedFromWindow
这一条也很好理解,如果有线程或者动画需要停止时,那么 onDetachedFromWindow 方法是一个很好的时机。当包含此 View 的 Activity 退出或者当前 View 被 remove 时,View 的 onDetachedFromWindow 方法会被调用,和此方法对应的是 onAttachedToWindow 方法,当包含此 View 的 Activity 启动时,View 的 onAttachedToWindow 方法会被调用。同时,当 View 变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。
- 5、View 带有滑动嵌套情形时,需要处理号滑动冲突
如果有滑动冲突的话,那么要合适地处理滑动冲突,否则将会严重影响 View 的效果,具体怎么解决请看我之前写的一篇博客:Android——View的事件体系(三)View的滑动冲突
自定义 View 示例
- 1、继承 View 重写 onDraw 方法
这种方法主要用于实现一些不规则的效果,一般需要重写 onDraw 方法。采用这种方法需要自己支持 wrap_content,并且 padding 也需要自己处理。 下面通过一个具体的例子来演示如果实现这种自定义的 View
为了更好地展示一些平时不容易注意到的问题,这里选择实现一个很简单的自定义控件,简单到只是绘制一个圆,尽管如此,需要注意的细节还是很多的。为了实现一个规范的控件,在实现过程中必须考虑到 wrap_content 模式以及 padding,同时为了提高便捷性,还要对外提供自定义属性。我们先来看一下最简单的实现,代码如下所示:
public class CoustornView_onDraw extends View {
private Paint mPaint;
private int mColor;
public CoustornView_onDraw(Context context) {
super(context);
init();
}
public CoustornView_onDraw(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CoustornView_onDraw(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mColor = Color.RED;
mPaint.setColor(mColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min(width, height) / 2;
canvas.drawCircle(width / 2, height / 2, radius, mPaint);
}
}
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context="com.layoutinflate.mk.www.custonview01_ondraw.MainActivity">
<com.layoutinflate.mk.www.custonview01_ondraw.ui.CoustornView_onDraw
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#000000" />
</RelativeLayout>
上面的代码很简单,主要用到了 Paint 和 Canvas 这两个类,更多关于这两个类的使用方式,我们会在 View 的工作原理系列文章完结后,专门拿出来几篇文章做详细介绍,敬请期待噢….
上面的代码实现了一个具有圆形效果的自定义 View,它会在自己的中心点以 宽/高的最小值为直径绘制一个红色的实心圆,效果图如下:
它的实现很简单,并且上面的代码相信对大家来说都是毛毛雨,但是不得不说,上面的代码并不是一个规范的自定义 View,为什么这么说呢?我们通过调整布局参数来对比一下。
<com.layoutinflate.mk.www.custonview01_ondraw.ui.CoustornView_onDraw
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000" />
运行后看一下效果,是不是和我们预期的一样?这说明 margin 属性是生效的。这是因为 margin 属性是由父元素控制的,因此不需要在 我们的自定义 View 中做特殊处理。这里还有点迷糊的朋友,可以回去看下 Android——View的工作原理(二) 在回忆一下,好了,效果图如下所示:
接着继续,我们已经为我们的自定义 View 设置了margin,那么我们要不要试一下padding呢?来,代码走起….
<com.layoutinflate.mk.www.custonview01_ondraw.ui.CoustornView_onDraw
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp" />
运行下看下效果,咦!怎么好像没什么变化呢?这是怎么回事?之所以会出现这种情况是因为我们在前面提到的直接继承自 View 和 ViewGroup 的控件,padding 是默认无法生效的,需要自己处理。效果图如下所示:
先不去管这个问题,我们再来看另一个问题,这些问题我们在后面进行介绍。在调整一下我们自定义 View 的布局参数,将其宽度设置为 wrap_content ,如下所示:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context="com.layoutinflate.mk.www.custonview01_ondraw.MainActivity">
<com.layoutinflate.mk.www.custonview01_ondraw.ui.CoustornView_onDraw
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp" />
</RelativeLayout>
运行下看下效果,我去!这又是什么情况?结果发现 wrap_content 并没有达到预期的效果,发现宽度使用 wrap_content 和使用 match_parent 并没有任何区别。的确是这样的,这一点在前面也已经提到过;对于直接继承子 View 的控件,如果不对 wrap_content 做特殊处理,那么使用 wrap_content 就相当于使用 match_parent。效果图如下所示:
为了解决上面提到的几种问题,我们需要做如下处理:
首先,针对 wrap_content 的问题,其解决方法在 前面的两篇博客中已经介绍过,不知道大家有没有印象,没有印象的也没关系,我们再来一遍。这里针对 wrap_content 的处理其实很简单,我们只需要指定一个 wrap_content 模式的默认宽/高即可。比如选择 200px 作为默认的宽/高。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
widthSize = mSize;
heightSize = mSize;
}else if (widthMode == MeasureSpec.AT_MOST){
widthSize = mSize;
}else if (heightMode == MeasureSpec.AT_MOST){
heightSize = mSize;
}
setMeasuredDimension(widthSize,heightSize);
}
OK,运行下看下效果:
是不是OK了,代码非常简单,这里就不在浪费篇幅了,下面我们来搞定 padding 的问题,其实关于 padding 咱们之前也已经解决过,再来一次。针对 padding 的问题,也很简单,只要在绘制的时候考虑一下 padding 即可,因此,我们需要对 onDraw 稍微做一下修改,修改后的代码如下所示:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int padding = getPaddingLeft(); //leftPadding == rightPadding == topPadding == bottomPadding
width = width - padding * 2;
height = height - padding * 2;
int radius = Math.min(width, height) / 2;
canvas.drawCircle(width / 2 + padding, height / 2 + padding, radius, mPaint);
}
上面的代码很简单,中心思想就是在绘制的时候考虑到 View 四周的空白即可,其中圆心 和 半径都会考虑到 View 四周的 padding,从而做相应的调整。效果图如下所示:
最后,为了让我们的 View 更加容易使用,很多情况下我们还需要为其提供自定义属性,想 android:layout_width 和 android:layout_height 这种以 android 开头的属性是系统自带的属性,那么如何添加自定义属性呢?这也不是什么难事,遵循如下几步即可:
第一步,在 values 目录下面创建自定义属性的 XML,比如 atts.xml,也可以选择类似于 attrs_circle_view.xml 等这种以 attrs_ 开头的文件名,当然这个文件名并没有什么限制,可以随意取名字。针对本例来说,选择创建 attrs.xml 文件,文件内容如下:
<resources>
<declare-styleable name="CoustornView_onDraw">
<attr name="circle_color" format="color"></attr>
</declare-styleable>
</resources>
在上面的 XML 中声明了一个自定义属性集合 “CoustornView_onDraw”,在这个集合里面可以有很多自定义属性,这里只定义了一个格式为 “color”属性的 “circle_color”,这里的格式 color 指的是颜色,除了颜色格式,自定义属性还有其他格式,比如 reference 是指资源 id,dimension 是指尺寸,而像 string、integer 和 boolean 这种是指基本数据类型。除了列举的这些还有其他类型,大家可自行了解。
第二步,在 View 的构造方法中解析自定义属性并做相应处理。对于本例来说,我们需要解析 circle_color 这个属性的值,代码如下所示:
public CoustornView_onDraw(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CoustornView_onDraw);
mColor = a.getColor(R.styleable.CoustornView_onDraw_circle_color,Color.RED);
a.recycle();
init();
}
这看起来很简单,首先加载自定义属性集合 CoustornView_onDraw,接着解析 CoustornView_onDraw 属性集合中的 circle_color 属性,它的 id 为 R.styleable.CoustornView_onDraw_circle_color。在这一步骤中,如果在使用时没有指定 circle_color 属性,那么就是采用 Color.RED 作为默认的颜色值,解析完自定义属性后,通过 recycle 方法来释放资源,这样 CoustornView_onDraw 中所做的工作就完成了。
第三步,在布局文件中使用自定义属性,如下所示:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context="com.layoutinflate.mk.www.custonview01_ondraw.MainActivity"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.layoutinflate.mk.www.custonview01_ondraw.ui.CoustornView_onDraw
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
app:circle_color="#00ff00"
android:background="#000000"
android:padding="20dp" />
</RelativeLayout>
上面的布局文件中有一点需要注意,首先,为了使用自定义属性,必须在布局文件中添加 schemas 声明:xmlns:app=”http://schemas.android.com/apk/res-auto”。在这个声明中,app 是自定义属性的前缀,当然也可以换其他名字,但是 CoustornView_onDraw 中的自定义属性的前缀必须和这里的一致,然后就可以在 CoustornView_onDraw 中使用自定义属性了,比如:app:circle_color=”#00ff00”。另外,也有按照如下方式声明 schemas : xmlns:app=”http://schemas.android.com/apk/res/com.mk.consutorn” 这种方式会在 apk/res 后面附加应用的包名。但是这两种方式并没有本质区别,个人比较喜欢前面那一种方式。
到这里自定义属性的使用过程就完成了,还没运行呢,跑起来看看效果:
下面给出完整代码:
public class CoustornView_onDraw extends View {
private Paint mPaint;
private int mColor;
private int mSize = 200;
public CoustornView_onDraw(Context context) {
this(context, null);
}
public CoustornView_onDraw(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public CoustornView_onDraw(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CoustornView_onDraw);
mColor = a.getColor(R.styleable.CoustornView_onDraw_circle_color,Color.RED);
a.recycle();
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(mColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
widthSize = mSize;
heightSize = mSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
widthSize = mSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
heightSize = mSize;
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int padding = getPaddingLeft(); //leftPadding == rightPadding == topPadding == bottomPadding
width = width - padding * 2;
height = height - padding * 2;
int radius = Math.min(width, height) / 2;
canvas.drawCircle(width / 2 + padding, height / 2 + padding, radius, mPaint);
}
}
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context="com.layoutinflate.mk.www.custonview01_ondraw.MainActivity"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.layoutinflate.mk.www.custonview01_ondraw.ui.CoustornView_onDraw
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
app:circle_color="#00ff00"
android:background="#000000"
android:padding="20dp" />
</RelativeLayout>
<resources>
<declare-styleable name="CoustornView_onDraw">
<attr name="circle_color" format="color"></attr>
</declare-styleable>
</resources>
- 2、继承 ViewGroup 派生特殊的 Layout
这种方式主要用于实现自定义额布局,采用这种方式稍微复杂一些,需要合适地处理 ViewGroup 的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
需要说明的是,如果采用此种方法实现一个很规范的自定义 View,是有一定的代价的,这点通过查看 LinearLayout 等的源码就知道,它们的实现都很复杂。对于我们接下来要实现的一个例子来说,这里并不打算实现它的方方面面,仅仅是完成主要功能,但是需要规范化的地方会给出说明。
这里我们先介绍下我们要实现的例子——HorizontalScrollViewEx 的功能,它主要是一个类似于 ViewPager 的控件,也可以说是一个类似于水平方向的 LinearLayout 的控件,它内部的子元素可以进行水平滑动并且子元素的内部还可以进行竖直滑动,这显然是存在滑动冲突的,但是 HorizontalScrollViewEx 内部解决了水平 和 竖直方向的滑动冲突问题。关于 HorizontalScrollViewEx 是如何解决滑动冲突的,请参看 Android——View的事件体系(三)View的滑动冲突 的相关内容。这里有一个假设,那就是所有子元素的宽/高都是一样的。
下面直接给出完整代码,代码中注释比较全,这里就不在过程叙述了…
//自定义 ViewGroup
public class HorizontalScrollViewEx extends ViewGroup {
private static final String TAG = "HorizontalScrollViewEx";
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
// 分别记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public HorizontalScrollViewEx(Context context) {
super(context);
init(context);
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
if (mScroller == null) {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
mChildWidth = MyUtils.getScreenMetrics(context).widthPixels;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
//返回 Scroller 是否已完成滚动
if (!mScroller.isFinished()) {
//停止动画。与forceFinished(boolean)相反,Scroller滚动到最终x与y位置时中止动画。
mScroller.abortAnimation();
intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
// 水平滑动距离差 大于 竖直滑动距离差,拦截当前点击事件
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
Log.d(TAG, "intercepted=" + intercepted);
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
}
case MotionEvent.ACTION_UP: {
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
int parentWidth = 0;
int parentHeight = 0;
final int childCount = getChildCount();
/**这里通过遍历子元素并调用 measureChildWithMargins 来处理子元素设置的margin
* 子元素的大小 == 父元素的大小 - 已用大小 - 父元素padding - 子元素margin */
for (int i = 0; i < childCount; i++) {
View childAt = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) childAt.getLayoutParams();
//对已知子元素进行margin、padding 测量
measureChildWithMargins(childAt, widthMeasureSpec, 0, heightMeasureSpec, 0);
//获取当前子元素的高度
measuredHeight = childAt.getMeasuredHeight();
}
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
//对父元素进行测量
if (childCount != 0) { //如果有子元素
//如果宽和高均为 wrap_content,则宽为 所有子元素的宽,高为最后一个子元素的高
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
measuredWidth = mChildWidth * childCount;
setMeasuredDimension(measuredWidth, measuredHeight);
} else if (heightSpecMode == MeasureSpec.AT_MOST) { //同上
setMeasuredDimension(widthSpaceSize, measuredHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) { //同上
measuredWidth = mChildWidth * childCount;
setMeasuredDimension(measuredWidth, heightSpaceSize);
} else { //采用默认
setMeasuredDimension(widthMeasureSpec, heightSpaceSize);
}
} else {
setMeasuredDimension(widthMeasureSpec, heightSpaceSize);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
final int childWidth = childView.getMeasuredWidth();
HorizontalScrollViewExLayoutParams layoutParams = (HorizontalScrollViewExLayoutParams) childView.getLayoutParams();
/**子元素坐标:
* left == 父元素的 paddingLeft + 子元素的 leftMargin + right + 父元素的paddingRight + 子元素的 rightMargin
* top == 父元素 padding + 子元素的 topMargin
* right == left + 子元素的宽度
* bottom == top + 子元素的高度
* */
int left = childLeft + getPaddingLeft() + layoutParams.leftMargin;
int top = getPaddingTop() + layoutParams.topMargin;
int right = left + childWidth;
int bottom = top + childView.getMeasuredHeight();
childView.layout(left, top, right, bottom);
Log.e("0000000", "childWidth=" + childWidth + "====left=" + left + "====top=" + top + "====right=" + right + "====bottom=" + bottom + "====childLeft=" + childLeft);
childLeft = right + getPaddingRight() + layoutParams.rightMargin;
}
}
}
private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
//该方法必须实现 详见:http://blog.csdn.net/qinjuning/article/details/8051811
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new HorizontalScrollViewExLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
//该方法必须实现 详见:http://blog.csdn.net/qinjuning/article/details/8051811
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new HorizontalScrollViewExLayoutParams(getContext(), attrs);
}
//该方法必须实现 详见:http://blog.csdn.net/qinjuning/article/details/8051811
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new HorizontalScrollViewExLayoutParams(p);
}
/**
* 自定义的内部 LayoutParams,如果要考虑子元素的margin,那么就必须要手动在我们自定义的ViewGroup中
* 实现自己的 LayoutParams 不然当我们在后面调用 measureChildWithMargins 方法时,会出现类型转换异常
*
* 具体原因为:当我们在为我们自定义的ViewGroup添加view时,本项目中采用的是addView(View viwe)方法,该方法
* 内部会默认为指定的view添加一个默认的LayoutParams(ViewGroup#LayoutParams),而当我们调用measureChildWithMargins
* 方法时,该方法的内部会将子元素的LayoutParams转换为 MarginLayoutParams 类型,而如果我们自定义 View 内部
* 没有 MarginLayoutParams 的实现类,那么就会抛出异常..... 详见:http://blog.csdn.net/qinjuning/article/details/8051811
*/
public static class HorizontalScrollViewExLayoutParams extends MarginLayoutParams {
public HorizontalScrollViewExLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public HorizontalScrollViewExLayoutParams(int width, int height) {
super(width, height);
}
public HorizontalScrollViewExLayoutParams(MarginLayoutParams source) {
super(source);
}
public HorizontalScrollViewExLayoutParams(LayoutParams source) {
super(source);
}
}
}
// 对应的 Activity
public class DemoActivity_2 extends Activity {
private static final String TAG = "DemoActivity_2";
private HorizontalScrollViewEx mListContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.demo_2);
Log.d(TAG, "onCreate");
initView();
}
private void initView() {
LayoutInflater inflater = getLayoutInflater();
mListContainer = (HorizontalScrollViewEx) findViewById(R.id.container);
final int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
final int screenHeight = MyUtils.getScreenMetrics(this).heightPixels;
for (int i = 0; i < 3; i++) {
ViewGroup layout = (ViewGroup) inflater.inflate(
R.layout.content_layout, mListContainer, false);
// layout.getLayoutParams().width = screenWidth;
TextView textView = (TextView) layout.findViewById(R.id.title);
textView.setText("page " + (i + 1));
layout.setBackgroundColor(Color.rgb(255 / (i + 1), 255 / (i + 1), 0));
createList(layout);
mListContainer.addView(layout);
}
}
private void createList(ViewGroup layout) {
ListView listView = (ListView) layout.findViewById(R.id.list);
ArrayList<String> datas = new ArrayList<String>();
for (int i = 0; i < 50; i++) {
datas.add("name " + i);
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
R.layout.content_list_item, R.id.name, datas);
listView.setAdapter(adapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Toast.makeText(DemoActivity_2.this, "click item",
Toast.LENGTH_SHORT).show();
}
});
}
}
//主布局
<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="match_parent"
android:background="#ffffff"
android:orientation="vertical" >
<com.ryg.chapter_4.ui.HorizontalScrollViewEx
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00ff00"
/>
</LinearLayout>
//子元素布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:text="TextView" />
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff4f7f9"
android:cacheColorHint="#00000000"
android:divider="#dddbdb"
android:dividerHeight="1.0px"
android:listSelector="@android:color/transparent" />
</LinearLayout>