自定义ViewGroup
如下图所示就是这次的自定义ViewGroup:
这次准备实现一个类似Android原生控件ScrollView的自定义ViewGroup,自定义ViewGroup可以实现ScrollView所具有的上下滑动功能,但是在滑动的过程中,增加一个粘性的效果,即当一个子View向上滑动大于一定的距离后,松开手指,它能自动的向上滑动,显示下一个子View。反之同理。
首先实现类似ScrollView的功能。
使用遍历的方式通知子View对自身进行测量,代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; i++) {
// 遍历查找所有的子view
View childView = getChildAt(i);
// 测量所有子view的宽、高
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
接下来,对子View进行位置的设定。让每个子View都显示完整的一屏,这样可以看出更好的效果。在放置子View前,需要确定整个ViewGroup的高度。由于子View都是占一个屏的高度,所以ViewGroup的高度就是子View的个数乘以屏幕的高度。我们通过如下代码来确定整个ViewGroup的高度:
// 设置ViewGroup的高度
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
mlp.height = mScreenHeight * childCount;
setLayoutParams(mlp);
获取了整个ViewGroup的高度后,就可以遍历并设定每个子View需要放置的位置了,直接通过调用子View的layout()方法,将位置作为参数传递进去即可,代码如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
// 设置ViewGroup的高度
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
mlp.height = mScreenHeight * childCount;
setLayoutParams(mlp);
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
child.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight);
}
}
}
在代码中主要是修改每个子View的top和bottom这两个属性,让它们能依次排序下来。
通过上面的步骤,就可以将子View放置到ViewGroup中了。但此时的ViewGroup还不能响应任何触控事件,自然也不能滑动,因此我们需要重写onTouchEvent()方法,为ViewGroup添加响应事件。在ViewGroup中添加滑动事件,通常可以使用scrollBy()方法来辅助滑动。在onTouchEvent()的ACTION_MOVE事件中,只要使用scrollBy(0,dy)方法,手指滑动的时候让ViewGroup中的所有子View也跟着滚动dy即可,计算dy的方法有很多,代码如下:
case MotionEvent.ACTION_DOWN: // 按下
// 记录本次左上角的坐标,为下次动作做准备
mLastY = y;
// 获取初始的y轴坐标
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE: // 滑动
// 判断是否完成动作
if (!mScroller.isFinished()) {
// 如果没有动作的话就立刻停止动画
mScroller.abortAnimation();
}
// 当前手指位置坐标减去没有动作时的坐标
int dy = mLastY - y; // 滑动距离dy
// 如果检测到坐标小于0,说明当前坐标是处于第一页,用户试图向下滑动
if (getScrollY() < 0) {
// 直接将滑动距离置为0
dy = 0;
}
// 如果超过了父view的高度减去子view的高度的话(处于最后一页),并且用户试图向上滑动
if (getScrollY() > getHeight() - mScreenHeight) {
// 直接将滑动距离置为0
dy = 0;
}
// 辅助滑动,执行滑动
scrollBy(0, dy);
// 记录左上角坐标,为下次动作做准备
mLastY = y;
break;
按如上方法操作就可以实现类似ScrollView的滚动效果了。当然,系统的原生ScrollView有更强大的功能,比较滑动的惯性效果等,这些功能可以在后面慢慢添加,这也是一个空间的迭代的过程。
最后实现控件滑动的黏性效果。要实现手指离开后ViewGroup的黏性效果,我们很自然的想到onTouchEvent()的ACTION_UP事件和Scroller类。通过ACTION_UP事件中判断手指滑动的距离,来确定是否执行滑动到下一个子View,代码如下所示:
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获得最左上角的坐标相对于 view 刚显示出来原点的位置
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: // 按下
// 记录本次左上角的坐标,为下次动作做准备
mLastY = y;
// 获取初始的y轴坐标
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE: // 滑动
// 判断是否完成动作
if (!mScroller.isFinished()) {
// 如果没有动作的话就立刻停止动画
mScroller.abortAnimation();
}
// 当前手指位置坐标减去没有动作时的坐标
int dy = mLastY - y; // 滑动距离dy
// 如果检测到坐标小于0,说明当前坐标是处于第一页,用户试图向下滑动
if (getScrollY() < 0) {
// 直接将滑动距离置为0
dy = 0;
}
// 如果超过了父view的高度减去子view的高度的话(处于最后一页),并且用户试图向上滑动
if (getScrollY() > getHeight() - mScreenHeight) {
// 直接将滑动距离置为0
dy = 0;
}
// 辅助滑动,执行滑动
scrollBy(0, dy);
// 记录左上角坐标,为下次动作做准备
mLastY = y;
break;
case MotionEvent.ACTION_UP: // 松手
// 获取结束点坐标
mEnd = getScrollY();
// 获取滑动距离
int dScrollY = mEnd - mStart;
// 判断执行滑动的方向(等于零可以防止双击滑动)
if (dScrollY >= 0) {
// 判断滑动距离是否符合翻页条件(滑动距离为屏幕的三分之一)
if (dScrollY < mScreenHeight / 3) {
// 不符合就回到初始位置
mScroller.startScroll(
0, getScrollY(),
0, -dScrollY
);
} else {
// 符合就向下滑动一整页
mScroller.startScroll(
0, getScrollY(),
// 向下辅助滑动的距离需要将子view的高度减去用户已经滑动过的距离
0, mScreenHeight - dScrollY
);
}
} else {
// 如果滑动的距离小于0,即手指向上滑动(-dScrollY相当于滑动距离,因为dScrollY小于0)
if (-dScrollY < mScreenHeight / 3) {
// 滑动距离,计算要注意滑动距离为负数
mScroller.startScroll(
0, getScrollY(),
0, -dScrollY
);
} else {
mScroller.startScroll(
0, getScrollY(),
0, -mScreenHeight - dScrollY
);
}
}
break;
}
// 刷新界面,区别于invalidate()
postInvalidate();
return true;
}
当然,最后不要忘记加上computeScroll()代码,如下所示:
@Override
public void computeScroll(){
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
}
代码
以下是完整代码:
package com.example.customvg;
import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Scroller;
/**
* 自定义的ViewGroup(类似原生控件ScrollView)
* bug比较多,过度调戏会卡死
* 建议还是尽量使用原生控件
* Created by shize on 2016/9/7.
*/
public class MyViewGroup extends ViewGroup {
// 屏幕高度
private int mScreenHeight;
private int mLastY;
private Scroller mScroller;
// 手指触碰终点
int mEnd;
// 手指触碰起始点
int mStart = 0;
public MyViewGroup(Context context) {
super(context);
initView(context);
}
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
/**
* 初始化View
* 图片太大可能会卡顿,不建议使用过大的图片
* @param context
*/
private void initView(Context context) {
// 获取窗口管理器
WindowManager mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
// 新建一个DisplayMetrics
DisplayMetrics mDisplayMetrics = new DisplayMetrics();
// 将度量标准设置为窗口管理器的默认显示
mWindowManager.getDefaultDisplay().getMetrics(mDisplayMetrics);
// 获取屏幕的宽度并赋值给mScreenHeight
mScreenHeight = mDisplayMetrics.heightPixels;
// 实例化一个滚动类Scroller
mScroller = new Scroller(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; i++) {
// 遍历查找所有的子view
View childView = getChildAt(i);
// 测量所有子view的宽、高
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
// 设置ViewGroup的高度
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
mlp.height = mScreenHeight * childCount;
setLayoutParams(mlp);
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
child.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获得最左上角的坐标相对于 view 刚显示出来原点的位置
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: // 按下
// 记录本次左上角的坐标,为下次动作做准备
mLastY = y;
// 获取初始的y轴坐标
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE: // 滑动
// 判断是否完成动作
if (!mScroller.isFinished()) {
// 如果没有动作的话就立刻停止动画
mScroller.abortAnimation();
}
// 当前手指位置坐标减去没有动作时的坐标
int dy = mLastY - y; // 滑动距离dy
// 如果检测到坐标小于0,说明当前坐标是处于第一页,用户试图向下滑动
if (getScrollY() < 0) {
// 直接将滑动距离置为0
dy = 0;
}
// 如果超过了父view的高度减去子view的高度的话(处于最后一页),并且用户试图向上滑动
if (getScrollY() > getHeight() - mScreenHeight) {
// 直接将滑动距离置为0
dy = 0;
}
// 辅助滑动,执行滑动
scrollBy(0, dy);
// 记录左上角坐标,为下次动作做准备
mLastY = y;
break;
case MotionEvent.ACTION_UP: // 松手
// 获取结束点坐标
mEnd = getScrollY();
// 获取滑动距离
int dScrollY = mEnd - mStart;
// 判断执行滑动的方向(等于零可以防止双击滑动)
if (dScrollY >= 0) {
// 判断滑动距离是否符合翻页条件(滑动距离为屏幕的三分之一)
if (dScrollY < mScreenHeight / 3) {
// 不符合就回到初始位置
mScroller.startScroll(
0, getScrollY(),
0, -dScrollY
);
} else {
// 符合就向下滑动一整页
mScroller.startScroll(
0, getScrollY(),
// 向下辅助滑动的距离需要将子view的高度减去用户已经滑动过的距离
0, mScreenHeight - dScrollY
);
}
} else {
// 如果滑动的距离小于0,即手指向上滑动(-dScrollY相当于滑动距离,因为dScrollY小于0)
if (-dScrollY < mScreenHeight / 3) {
// 滑动距离,计算要注意滑动距离为负数
mScroller.startScroll(
0, getScrollY(),
0, -dScrollY
);
} else {
mScroller.startScroll(
0, getScrollY(),
0, -mScreenHeight - dScrollY
);
}
}
break;
}
// 刷新界面,区别于invalidate()
postInvalidate();
return true;
}
@Override
public void computeScroll(){
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
}
}
布局文件的完整代码:
<?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="match_parent"
android:orientation="horizontal"
tools:context="com.example.customviewgroup.MainActivity">
<com.example.customvg.MyViewGroup
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/test1"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/test2"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/test3"/>
</com.example.customvg.MyViewGroup>
</LinearLayout>
感谢阅读,下次再见。