`
小骏向前冲
  • 浏览: 18293 次
社区版块
存档分类
最新评论

自定义ViewGroup实现滑屏等动作

阅读更多
自定义ViewGroup实现滑屏等动作
完全了解这部分知识后不但可以实现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,这就构成了一个递归调用的过程,流程图:


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;
}
}
运行效果: 

看,刚才添加的两个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;
}

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

一、 Scroll类介绍
之前介绍了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类可以帮助实现类似动画的效果

二、computeScroll()方法介绍
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之后被回调了,但是在这里我尝试了一下,如果也是直接用return mDetector.onTouchEvent(ev)的方式,onFling事件无法响应,打log发现return mDetector.onTouchEvent(ev)为false,按照我的理解,onTouchEvent的Down动作返回false后,不会继续收到move和up消息,而onFling是要在up消息中回调的,这就很正常了,但这又让我不明白了,为什么我们单独实现手势的时候onFling事件又能响应呢??请哪位童鞋有知道的告诉我。
在此感谢短裤党大牛的博客,我也是参照着大牛的博客写的,算是学习总结吧
分享到:
评论

相关推荐

    android 滑屏

    本文将详细讲解如何通过两种方法实现滑屏:重写ViewGroup和使用ViewPager。 首先,我们来了解一下重写ViewGroup的方式。ViewGroup是Android布局管理器的基础类,它允许你自定义布局行为。在滑屏场景中,我们可以...

    android 左右滑屏

    要实现左右滑屏,我们需要创建一个自定义的View或ViewGroup,例如ScrollView或ViewPager。在自定义的View中,我们需要处理触摸事件,通过GestureDetector检测滑动,然后利用Scroller进行平滑滚动。同时,还需要维护...

    Launcher滑屏动画详解

    9. **自定义动画**:开发者可以通过覆写`drawChild`方法,结合`Transformation`对象,以及控制Canvas的绘制行为,实现自定义的滑屏动画效果。 10. **性能优化**:在处理大量子View时,应考虑性能优化,比如避免不必...

    上下滑屏控件

    上下滑屏控件在移动应用开发中非常常见,主要用于实现用户通过上下滑动来浏览内容的效果,例如在新闻阅读、社交媒体、电商应用等场景。在Android系统中,我们可以使用多种方式来实现这样的功能,其中自定义View是...

    android 触摸滑屏

    在自定义View中,可以重写`onTouchEvent()`,根据MotionEvent的动作序列判断是否为滑动操作。如果识别出滑动,可以通过改变View的位置来实现滑动效果。例如,ListView和RecyclerView就是通过这种方式实现了列表的...

    滑动切换Activity Demo Eclipse

    在Android开发中,这种效果可以通过自定义ViewGroup或者使用现有的库来实现。本Demo是基于Eclipse开发的一个实例,它展示了如何实现类似百度贴吧的滑动切换Activity的效果,支持四种基本手势:左滑、右滑、上滑和...

    Android Moveview滑屏移动视图类完整实例

    `Moveview` 类就是这样一个专门为实现滑动切换视图功能而设计的自定义视图组件。在这个实例中,我们将深入理解 `Moveview` 类的工作原理以及如何使用它。 首先,`Moveview` 类继承自 `ViewGroup`,这意味着它可以...

    多页滑屏手势

    `ViewFlipper`是`ViewGroup`的一个子类,它能够管理一组子视图,并自动在它们之间进行切换。默认情况下,`ViewFlipper`会显示第一个子视图,然后可以通过编程或手势来改变当前显示的视图。要创建一个`ViewFlipper`,...

Global site tag (gtag.js) - Google Analytics