自定义ViewGroup实现滑屏等动作

[size=large][color=green]自定义ViewGroup实现滑屏等动作 [/color][/size]
完全了解这部分知识后不但可以实现gallerry,滑屏,微信登录界面等效果,还可以实现类似3D旋转等很炫的UI(这个要结合Camera类继续深入学习)。
首先明白ViewGroup是继承View,所以它有view的一部分属性和方法,你也可以把它看作一个View,ViewGroup可以嵌套,可以往里面添加各种各样的View,先来看个小demo

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new MyViewGroup(this));
}

}

public class MyViewGroup extends ViewGroup {

public MyViewGroup(Context context) {
super(context);
Button button1 = new Button(context);
button1.setText("button1");

Button button2 = new Button(context);
button2.setText("button2");

TextView textView = new TextView(context);
textView.setText("textView");

addView(button1);
addView(button2);
addView(textView);
}

@Override
protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
// TODO Auto-generated method stub

}
}



自定义的ViewGroup中新添加了两个Button和一个TextView,运行程序,却看不到任何东西,这是为什么呢?我们可以看到在创建自定义的ViewGroup类时,程序自动重写了方法onLayout(),该方法干嘛用的?
源码中ViewGroup::onLayout只是个抽象方法,并无任何实现,它被View::layout调用,View的layout(int left,int top,int right,int bottom)方法负责把该view放在参数指定位置,所以如果我们在自定义的ViewGroup::onLayout中遍历每一个子view并用view.layout()指定其位置,每一个子View又会调用onLayout,这就构成了一个递归调用的过程,流程图:
[img]http://dl.iteye.com/topics/download/b1fba3f0-e155-3aac-a4db-12652250e224[/img]

Demo中重写onLayout()
protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
int childCount = getChildCount();
int left = 0;
int top = 10;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
child.layout(left, top, left + 60, top + 60);
top += 70;
}
}
运行效果:
[img]http://dl.iteye.com/topics/download/6bfff81e-8914-3ebb-a564-bad76ca49b57[/img]
看,刚才添加的两个Button和一个TextView都出来了吧。简单来说,自定义ViewGroup时必须重写ViewGroup的onLayout方法去遍历每一个子View,将其摆在理想位置。

ViewGroup还有很多常用的重写方法,比如onMeasure,看下面这段代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int childCount = getChildCount();
//设置该ViewGroup的大小
int specSize_width = MeasureSpec.getSize(widthMeasureSpec);
int specSize_height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(specSize_width, specSize_height);

for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
childView.measure(80, 80);
}
}

通过重写onMeasure方法不但可以为ViewGroup指定大小,还可以通过遍历为每一个子View指定大小,在自定义ViewGroup中添加上面代码为ViewGroup中的每一个子View分配了显示的宽高。


到现在ViewGroup中的每个子View都显示出来了,接下来就是让它们都动起来,那该怎么动呢?
我们都知道动画之所以会动是因为不停地切换底片,通过把一个个微小的变化连起来达到动的效果,在View中也一样,View并不会自己跑,只是通过不停地给它指定一个新位置来达到动的效果。

先简单说一下scrollTo()和scrollBy()方法:
源码:
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}

ScrollTo(int x,int y)
指定该View在当前视图中偏移至(x,y)处,调用ScrollTo不但会回调onScrollChanged方法,还会引起重绘。
按照我的理解,视图即当前的可视区域(假设只有两层:父视图和子视图),子视图的位置可以是任意的,以父视图左上角为(0,0),一旦子视图处于父视图之外(比如-5,-5),那位于(-5,-5,0,0)这部分的子视图就不可见了。

/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}

显然ScrollBy(x,y)是把当前视图在原位置基础上移动(x,y)个坐标,同样会引起重绘。

运用ScrollTo()和ScrollBy()方法在开始的demo中重写onTouchEvent()函数:
public boolean onTouchEvent(MotionEvent ev) {
final float y = ev.getY();
switch(ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// if (!mScroller.isFinished()) {
// mScroller.forceFinished(true);
// move = mScroller.getFinalY();
// }
mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
int detaY = (int)(mLastMotionY - y);
mLastMotionY = y;
// offset += detaY;
scrollBy(0, detaY);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}

此时界面就会随着你手指的拨动来上下滑动了,很奇妙是不是,但是你会发现界面的上下滑动显得很死板,手指拨的再快界面也滑不了多远,这用户体验太差了!该怎么做呢?

[color=darkblue][size=large]一、 Scroll类介绍[/size][/color]
之前介绍了scrollTo()和scrollBy()方法,这两个方法都可以实现view的移动,但这两个方法都是让该view一步到位的,完全看不到中间的运动过程,虽然可以配合着onTouchEvent来达到慢慢移动的效果,但如果不是手指滑动屏幕的情况下该怎么办呢,根本不可能嘛,幸好源码中提供了Scroll这个类来帮助我们实现偏移控制。

到源码中分析Scroll类:
public class Scroller  {
private int mStartX;
private int mStartY;
private int mFinalX;
private int mFinalY;

private int mCurrX;
private int mCurrY;
private long mStartTime;
private int mDuration;

public Scroller(Context context) {
this(context, null);
}

//返回当前X坐标
public final int getCurrX() {
return mCurrX;
}

//返回当前Y坐标
public final int getCurrY() {
return mCurrY;
}

//返回开始X坐标
public final int getStartX() {
return mStartX;
}

//返回开始Y坐标
public final int getStartY() {
return mStartY;
}

//返回停下来的X坐标
public final int getFinalX() {
return mFinalX;
}

//返回停下来的Y坐标
public final int getFinalY() {
return mFinalY;
}

//如果finished值为true,则强行结束本次滑屏操作
public final void forceFinished(boolean finished) {
mFinished = finished;
}

/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
//根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
....
....
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}


//开始一个动画控制,从(startX,startY)出发在duration时间内前进(dx,dy)个单位
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}

//根据滑动手势开始滑动,滑动距离由初始速度决定
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
// Continue a scroll or fling in progress
if (mFlywheel && !mFinished) {
float oldVel = getCurrVelocity();
....
....
}
}
}


Scroll类可以帮助实现类似动画的效果

[color=darkblue][size=large]二、computeScroll()方法介绍[/size][/color]
android框架提供了computeScroll方法来控制滑屏的过程。
View的绘制方法draw()中调用了computeScroll(),而computeScroll()自身并没有做任何操作,所以这个是要自己来实现的,view的重绘会调用draw()方法,所以view的每次变化都会调用到computeScroll()方法,我们可以在自定义的View/ViewGroup中配合刚介绍的Scroll类去重写computeScroll方法,来实现偏移控制。

在ViewGroup中重写computeScroll():
public void computeScroll() {
/*Scroller类里的computeScrollOffset不但返回Scroller是否已经停止
而且计算了Scroller类里的属性值,下面getCurrY得到的值就是在这里计算出来的 */
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
//view一旦重绘就会回调onDraw,onDraw中又会调用computeScroll,不停绘制
postInvalidate();
}
}


我们经常可以在一些app中发现手指轻轻一滑,界面飞速移动的效果,可以用到上面介绍的Scroll类并结合手势检测(GestureDetector的知识这里不作介绍了)来实现,主要方法是在重写的onFling()方法中把参数中的X/Y轴上的速度参数velocityX/velocityY传给Scroll.fling(),让view继续滑动

@Override
public boolean onTouchEvent(MotionEvent ev) {
final float y = ev.getY();
switch(ev.getAction()) {
//按下时,如果还在滑动则立即停止
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
move = mScroller.getFinalY();
}
mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
int detaY = (int)(mLastMotionY - y);
mLastMotionY = y;
offset += detaY;
scrollBy(0, detaY);
break;
case MotionEvent.ACTION_UP:
break;
}
mDetector.onTouchEvent(ev);
return true;
}

@Override
public boolean onFling(MotionEvent arg0, MotionEvent arg1, float arg2,
float velocityY) {
int slow = (int) velocityY * 3 / 4;
mScroller.fling(0, offset, 0, slow, 0, 0, 0, 1000);
return false;
}

上面的fling方法从offset的位置开始,Y轴方向上移动初始速度为slow,移动最大距离为1000,至于实际移动距离完全是由Scroller类内部计算的,我们不必关心。
值得一提的是,一般我们实现GestureDetector. OnGestureListener都是在onTouchEvent这样返回:
public boolean onTouchEvent(MotionEvent ev) {
return mDetector.onTouchEvent(ev);

这样onFling事件就会在 MotionEvent.ACTION_UP之后被回调了,[size=medium]但是在这里我尝试了一下,如果也是直接用return mDetector.onTouchEvent(ev)的方式,onFling事件无法响应,打log发现return mDetector.onTouchEvent(ev)为false,按照我的理解,onTouchEvent的Down动作返回false后,不会继续收到move和up消息,而onFling是要在up消息中回调的,这就很正常了,但这又让我不明白了,为什么我们单独实现手势的时候onFling事件又能响应呢??请哪位童鞋有知道的告诉我。[/size]
[color=red][size=large]在此感谢短裤党大牛的博客,我也是参照着大牛的博客写的,算是学习总结吧 :D [/size][/color]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值