PullScrollView详解(二)——Animation、Layout与下拉回弹

前言:这几天能好好地把自己想学的知识好好整理一遍,感觉真是好极了。时间不多了,是要到最后拼搏的时间了。相信自己,你永远是最棒的。

 

相关文章:

1、《PullScrollView详解(一)——自定义控件属性》
2、《PullScrollView详解(二)——Animation、Layout与下拉回弹》
3、《PullScrollView详解(三)——PullScrollView实现》
4、《PullScrollView详解(四)——完全使用listview实现下拉回弹(方法一)》
5、《PullScrollView详解(五)——完全使用listview实现下拉回弹(方法二)》
6、《PullScrollView详解(六)——延伸拓展(listview中getScrollY()一直等于0、ScrollView中的overScrollBy)》

 

这篇先给大家梳理下有关TranslateAnimation和Layout的知识点,然后我们初步做一个下拉回弹的效果出来。

一、TranslateAnimation和Layout

我们这里只讲这里所能用到的最重要的知识点,有关各个TranslateAnimation详细的使用方法,可以参考我以前写的博客系列:<Animation动画详解(三)—— 代码生成alpha、scale、translate、rotate、set及插值器动画>
 

1、TranslateAnimation

 

 

public TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta)

其中
fromXDelta:是指开始运动的起点的X坐标
toXDelta:指结束运动的终点的X坐标
fromYDelta:指开始运动的起点的Y坐标
toYDelta:指结束运动的终点的Y坐标

使用方法:

TranslateAnimation animation = new TranslateAnimation(0, 0, 0, -100);
animation.setDuration(200);
view.startAnimation(animation);

这段代码的意思是,将view从(0,0)位置移动的(0,-100)的位置,也就是向上移动100像素。
那这里有个疑问:移动所按的坐标系是哪一个?是view父控件的还是自己的坐标系?
当然是按照自己的坐标系,从最后一句代码也可以看出来:

view.startAnimation(animation);

这个animation是运用在view中的,当然是以view的左上角为坐标系的原点来计算运动坐标的。理解不了也没关系,下面我们会用个例子来详细讲一讲。

2、Layout

先看看layout的声明:

 

 

public void layout(int left, int top, int right, int bottom)

对应的使用方法:

view.layout(view.getLeft(),view.getTop()+100,view.getRight(),view.getBottom()+100);

ayout函数有四个参数,分别是上、下、左、右,四个点的坐标。很明显,这是用来定位view所有位置的。
即上面的函数,即是将上和底分别向下移动了100像素。左、右的坐标不变。
这里要注意的是,它移动的坐标是按谁的来呢?我们这里传进去的值的时候,是通过类似view.getLeft()来获取左顶点X坐标的。这个坐标就相对父控件的坐标系的。所以layout的坐标是以父控件的坐标系来计算的。
其实这也好理解,因为layout可以随意的根据用户传进去的坐标来改变大小和位置。如果使用自己的坐标系来改变大小和位置的话,会很难计算;使用别人做为参照物来改变自己就相对容易的多。所以谷歌那帮老头用的是别人的坐标系来改变自己的位置和大小的。所以,一般情况下,改变自己位置和大小的函数,所用到的坐标系,一般都是父控件的坐标系;
 

3、示例

下面我们就来写个例子来试验一下TranslateAnimation的动画构造Animation时使用的坐标是通过自己的坐标系计算的。
下面是效果图:

 

首先,点击animation_up按钮,底部的TextView的移动动画为:

 

TranslateAnimation(0, 0, 0, -100);

即从(0,0)点移动到(0,-100)点;从图中也确实可以看到TextView从原始位置向上移动了;从图中也可以看到开始运动的(0,0)点,就是控件所有的原始位置。因为如果不在它原始位置的话,TextView会先移动到这个位置开始动画。
然后,我们点击layout_down按钮,将TextView控件通过layout函数将其下移100像素:

mRootView.layout(mRootView.getLeft(),mRootView.getTop()+100,mRootView.getRight(),mRootView.getBottom()+100);

这时候再点击animation_up按钮,同样是让它从(0,0)点运动到(0,-100)点;但它已经不是从上次的原始位置来开始运动了。而是从当前的控件位置向上移动;从这个示例可以看出:layout()函数,不仅能通过指定上、下、左、右四个点坐标来指定控件的大小和位置。同时,该控件坐标系的(0,0)原点,也随着左上角位置的变动而变动。即控件的左上角始终是该控件坐标系的(0,0)原点;
好了,下面我们就看看上面例子的代码实现吧:
(1)activity_main.xml
从效果图也可以看出,布局很简单,使用的垂直布局,依次三个控件而已:

<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="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/layout_down"
        android:text="layout_down"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/animation_up"
        android:text="animation_up"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/tv"
        android:layout_width="200dp"
        android:layout_height="400dp"
        android:layout_gravity="center_horizontal"
        android:background="#ff00ff"/>
</LinearLayout>

2、MainActivity.java
先看看整体代码:

 

public class MainActivity extends Activity implements View.OnClickListener{

    private TextView mRootView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mRootView = (TextView)findViewById(R.id.tv);

        Button btnMoveDwn = (Button)findViewById(R.id.layout_down);
        Button btnAnimationUp = (Button)findViewById(R.id.animation_up);

        btnMoveDwn.setOnClickListener(this);
        btnAnimationUp.setOnClickListener(this);
    }

    /**
     * 两个点:
     * 首先mRootView.startAnimation()中Animation的移动坐标是以mRootView的布局为原点的,如果使用mRootView.layout()将其改变,那么原点位置也将会改变.
     * Animation不会改变布局的坐标;layout则不一样,在改变位置的同时,layout的左上角始终是这个布局的(0,0)原点
     *
     * @param v
     */
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.layout_down:{
                mRootView.layout(mRootView.getLeft(),mRootView.getTop()+100,mRootView.getRight(),mRootView.getBottom()+100);
            }
            break;
            case R.id.animation_up:{
                TranslateAnimation animation = new TranslateAnimation(0, 0, 0, -100);
                animation.setDuration(200);
                mRootView.startAnimation(animation);
            }
            break;
        }
    }
}

核心的代码我们都讲过了,这里也没什么好讲的了;
通过这个例子,我们要知道:Animation使用的坐标系是以自己控件的左上角为原点的自己的坐标系;而layout函数则会改变控件大小和位置,在改变位置的同时,也改变了控件的坐标系,即layout的左上角始终是这个布局的(0,0)原点;

 

源码在文章底部给出

二、下拉回弹

上面把用到的两个函数给大家讲了讲,下面我们就看看怎么用这两个函数来做一个简单的下拉回弹的效果吧。先看看效果图:

从图中可以看到,当我们下拉猫咪图片之后,图片自动反弹回去。
这里涉及两个问题:

  • 怎么让布局跟随手指下移而下移
  • 怎么让布局反弹回去

首先,第一个问题:怎么让布局跟随手指下移而下移?
上面我们讲了,通过layout()函数可以改变布局的位置,而且,通过onTouchEvent()可以捕捉到手指的移动方向和距离。所以在onTouchEvent()中根据手指的移动距离,通过layout()函数将布局跟随移动即可。
然后,第二问题:怎么让布局反弹回去?
很明显,我们可以使用Animation动画,将布局从当前位置回到原来的位置即可。但这里需要注意的一点是,我们上面讲了,通过layout()函数在移动布局的同时,把布局的控件坐标系也改变了,即布局当前位置的左上角始终为该控件坐标系的原点位置。而Animation的坐标却又是根据该布局的左上角位置(原点)来计算的。
所以,在构造Aimation动画时,传入的运动参数要特别注意。这点, 我们会结合代码具体讲。

 

1、XML布局(activity_main.xml)

从上面的效果图中也可以看到,首先,我们的布局是可以滚动的,所以肯定会有ScrollView控件,然后在ScrollView中,各个控件是垂直布局的。
所以,activity_main.xml的布局代码如下:

 

 

 

 

 

<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="vertical"
    tools:context=".MainActivity">

    <com.harvic.com.touchscrollview.CustomScrollView
        android:layout_width="fill_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:orientation="vertical">
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:scaleType="fitXY"
                android:src="@drawable/pic1"/>
            <TextView
                android:text="extra item 1"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="16dp"/>
            <TextView
                android:text="extra item 2"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="16dp"/>
            <TextView
                android:text="extra item 3"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="16dp"/>
            <TextView
                android:text="extra item 4"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="16dp"/>

        </LinearLayout>

    </com.harvic.com.touchscrollview.CustomScrollView>

</LinearLayout>

其中CustomScrollView是下面我们将要自定义的控件,派生自ScrollView;上面的布局代码很容易看懂,就不再细讲。

2、布局跟随手指移动

我们首先要完成让布局跟随手指下拉而移动的操作。所以我们要在onTouchEvent()中捕捉手指的移动距离,通过layout()函数,让布局跟着移动
(1)布局初始化

private View mRootView;
@Override
protected void onFinishInflate() {
	// TODO Auto-generated method stub
	mRootView = getChildAt(0);
	super.onFinishInflate();
}

由于ScrollView只能有一个子布局,所以,我们通过getChildAt(0)直接获取ScrollView下的子布局即可;
onFinishInflate()函数,明显是在布局解析结束后会调用的函数。
(2)onTouchEvent拦截
让布局跟随手指移动的代码如下:

private int mpreY=0;
public boolean onTouchEvent(MotionEvent event) {
	float curY = event.getY();	
	switch (event.getAction()) {
	case MotionEvent.ACTION_MOVE:{
		int delta = (int)((curY - mpreY)*0.25);
		if (delta>0) {
			mRootView.layout(mRootView.getLeft(), mRootView.getTop()+delta, mRootView.getRight(), mRootView.getBottom()+delta);
		}
	}
	break;
	}
	mpreY = (int) curY;
	return super.onTouchEvent(event);
}

首先,通过mpreY来保存每次手指移动的最新位置。
然后,通过 event.getY() - mpreY就可以得到当前手指的移动距离
而我们的代码中却是这么写的:

float curY = event.getY();	
int delta = (int)((curY - mpreY)*0.25);

为什么这里要乘以0.25?
这是因为,如果我们不想让布局的移动距离跟手指的移动距离等同。让布局的移动距离比手指移动距离小,会给用户一种有阻力的感觉。这样,用户体验比较好。比较符合常规:当下拉一个东西的时候,总是感觉是有阻力的。这个定义阻力大小的数值就叫阻尼系数。这个值大家可以随便定义,越小,代表阻力越大。即布局实际移动距离就越小。
最后,是根据delta移动布局:

 

mRootView.layout(mRootView.getLeft(), mRootView.getTop()+delta, mRootView.getRight(), mRootView.getBottom()+delta);

由于,我们不改变大小,也不改变左右的位置,只让它向下移动,所以只需要在top和bottom上直接加上要移动的距离即可。
到目前为止,完整的CustomScrollView.java的代码为:

 

public class CustomScrollView extends ScrollView {
	// 根布局视图
	private View mRootView;
	private int mpreY = 0;

	public CustomScrollView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
	}

	@Override
	protected void onFinishInflate() {
		// TODO Auto-generated method stub
		mRootView = getChildAt(0);
		super.onFinishInflate();
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		float curY = event.getY();
		switch (event.getAction()) {
		case MotionEvent.ACTION_MOVE: {
			int delta = (int) ((curY - mpreY) * 0.25);
			if (delta > 0) {
				mRootView.layout(mRootView.getLeft(), mRootView.getTop()
						+ delta, mRootView.getRight(), mRootView.getBottom()
						+ delta);
			}
		}
			break;
		}
		mpreY = (int) curY;
		return super.onTouchEvent(event);
	}

}

3、回弹

回弹可能是理解起来难度相对比较大的一个地方,因为这里牵涉到动画的坐标问题。我们先看代码是怎么写的吧。
(1)、保存初始化位置
首先,我们在上面移动布局以前,把布局原来的位置保存起来,这样,我们回弹的时候就知道回到哪了。这个操作,我们在MotionEvent.ACTION_DOWN里完成:

//原始根视图对应的位置
private Rect mNormalRect;
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:{
            if (mRootView != null) {
                mNormalRect = new Rect(mRootView.getLeft(), mRootView.getTop(), mRootView.getRight(), mRootView.getBottom());
            }
        }
        break;
        …………
}

由于我们布局的初始化位置是不变的,所以,我们完全可以在其它地方获取原始布局的上、下、左、右位置,但由于布局的位置只有在真正被渲染出来以后才能得到,所以在 onFinishInflate()中是得不到的,因为这时候布局还没有被渲染出来。而当运行到onTouchEvent()时,布局肯定已经被渲染出来了。所以我放在了用户点击时获取初始化位置。
(2)、回弹

 

 

	public boolean onTouchEvent(MotionEvent event) {
		float curY = event.getY();
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:{
            if (mRootView != null) {
                mNormalRect = new Rect(mRootView.getLeft(), mRootView.getTop(), mRootView.getRight(), mRootView.getBottom());
            }
        }
        break;
		…………
		case MotionEvent.ACTION_UP:{
			int curTop = mRootView.getTop();
			mRootView.layout( mNormalRect.left, mNormalRect.top, mNormalRect.right, mNormalRect.bottom);
			TranslateAnimation animation = new TranslateAnimation(0, 0, curTop - mNormalRect.top, 0);
            animation.setDuration(200);
            mRootView.startAnimation(animation);
		}
		break;
		}
        mpreY = (int) curY;
		return super.onTouchEvent(event);
	}

非常注意在MotionEvent.ACTION_UP时的代码处理步骤:
1、使用layout将布局还原到原始位置
2、调用TranslateAnimation将布局从跟随手指移动到的位置运动到初始化位置。

代码运行的流程图如下:

 

首先,我们假定mRoot布局的初始化位置为A,即如图(2)中的位置所示;跟随手指移动到的位置为B,如图(1)中的位置所示;
整个代码的流程是这样的:
图示(1)显示的是,在MotionEvent.ACTION_MOVE中,让布局跟随手指移动后的状态。
图示(2),(3)显示的是反弹时的代码操作,即在MotionEvent.ACTION_UP中的代码。
这里非常要注意的一点是,我们反弹时,并不是只利用TranslateAnimation来将mRoot布局动画返回回去。
在操作动画之前,我们利用layout()函数,将布局首先还原到了初始化位置。

为什么要这样做呢?
我们在开头讲过,Animation动画的坐标是以它自己布局的左上角为顶点来计算的。如果我们只是利用动画将mRoot布局返回回去,那它本身的坐标系仍然没有还原。这跟初始化状态是不一样的。由于跟初始状态不一样,那以下次再下拉的时候,根据初始状态来做的各种操作就肯定会出问题。所以,无论怎样,我们都需要利用layout()将布局还原。

第二个问题,你先还原布局,然后再利用TranslateAnimation让布局返回到跟随手指移动到的位置(B位置),这样做,不会闪屏吗?
由于layout()函数移动布局是瞬间完成的,并不会像Animation一样,它没有过程,直接到达目的地。而TranslateAnimation移动到动画开始移动的点的过程中(到B点),也是瞬间完成的;TranslateAnimation的动画,只存在于用户定义的动画开始位置到动画结束位置的过程中。其它地方都是瞬间完成的。所以由于从B到A是瞬间完成的(图示1到图示2的过程),从B到A也是瞬间完成的(从图示2到图示3的过程),所以从图1到图3,用户看起来就是没有改变的。因为人眼要存在图象,必须图像的存在时间长于1/25秒,这才能被人眼采集进去,形成图像。而现在手机从图示(1)到图示(3)的处理时间,远低于我们人眼所能看到的时间,所以用户是根本看不到的。

源码在文章底部给出

三、网上解决方案及存在问题

大家可能看过网上的其它文章,在回弹的时候,我跟他们的方式不一样,大家可能会有疑问:网上很多人的回弹代码是下面这么写的?你为什么不这么写呢?

1、概述

网上的代码:(千篇一律)

 

 

TranslateAnimation ta = new TranslateAnimation(0, 0, curTop,normal.top);  
ta.setDuration(200);  
inner.startAnimation(ta);  
// 设置回到正常的布局位置  
inner.layout(normal.left, normal.top, normal.right, normal.bottom);

这段代码是不正确的,首先给大家说他的一个BUG,如果

TranslateAnimation ta = new TranslateAnimation(0, 0, curTop,normal.top);

中的normal.top不等于0,也就是说,你可以给mRootView添加上一个参数andoird:marginTop=”100dp”,你会明显的发现动画会出现问题。(源码会在文章底部给出)
添加位置如下:
即直接在MyScrollView下,我们要移动的布局,即mRoot所对应的XML控件,这里是LinearLayout

 

我们来对比原添加前后的效果图:
添加前:

添加后

明显看到,在添加以后的示图中,当手指松开以后,mRoot视图会先往下运动一下,然后再向上运动。我们后面会讲它的这个BUG要怎么解,但我们先来看一个更重要的问题。

 

2、问题一:为什么先动画后还原依然没问题?

再来看他回弹的代码:

 

 

TranslateAnimation ta = new TranslateAnimation(0, 0, curTop,normal.top);  
ta.setDuration(200);  
inner.startAnimation(ta);  
// 设置回到正常的布局位置  
inner.layout(normal.left, normal.top, normal.right, normal.bottom);  

首先,我们要明白的一个问题是,在MotionEvent.ACTION_MOVE中,我们已经利用layout()函数,将布局随手指移动到了指定位置(B位置)
我们上次说过,Animation动画的坐标是以自己左上角来算的,那么,也就是说当前的Animation坐标是以mRoot在B位置的坐标来算的。看下图

 

图(1)显示的是控件初始化位置。所示的O点是mRoot的原点的坐标。对应的值是(normal.left,normal.top,normal.right,normal.bottom)
当mRoot布局跟着手指下移,到图(2)的位置,由于我们移动布局使用的是layout()函数,所以在图(2)中,mRoot的左上角原点O,所对应的位置就是(mRoot.getLeft(),mRoot.getTop(),mRoot.getRight(),mRoot.getBottom),
如果在抬起手指时,先运行:

 

TranslateAnimation ta = new TranslateAnimation(0, 0, mRoot.getTop(),normal.top);  

上面我们讲过Animation的动画坐标是以控件本身左上角来计算的,所以它移动的位置应该如图(3)所示,而我们为什么没有看出下先向下移动然后再向上移动呢?
这是为什么呢,我考虑了很久,猜想原因应该是这样的:再来看看他反弹的代码:

TranslateAnimation ta = new TranslateAnimation(0, 0, mRoot.getTop(),normal.top);  
ta.setDuration(200);  
mRoot.startAnimation(ta);  
// 设置回到正常的布局位置  
mRoot.layout(normal.left, normal.top, normal.right, normal.bottom); 

注意,他在动画以后,直接利用mRoot.layout()将动画还原。我觉得,由于mRoot.layout()的操作是一瞬间的事情,而动画的渲染速度要比mRoot.layout()慢,所以当开始动画的时候,mRoot就已经回到了初始化的布局状态。此时的原点,依然是初始化状态的原点。所以看起来没有什么问题,但这完全是投机的算法。不是正规流程。

3、问题二:为什么给mRoot添加上anroid:margin_top:"100dp"以后,松手时会先向下运动一下?

 

这个问题依然很难讲,我都有点想放弃讲解了,还是说说吧,大家能看得懂就最好了,看不懂就算了……还是不要为难自己了……
最关键的问题还是在松手时,Animation动画的坐标是以本身的左上角为原点来计算的。

 

TranslateAnimation ta = new TranslateAnimation(0, 0, mRoot.getTop(),normal.top);  
ta.setDuration(200);  
mRoot.startAnimation(ta);  
// 设置回到正常的布局位置  
mRoot.layout(normal.left, normal.top, normal.right, normal.bottom); 

还是来看图吧:

 

图示1:显示的是mRoot的初始化位置,控件上面的空白,就是margin_top的高度
图示2:显示的是跟随手指移动的最终位置。此时利用mRoot.getTop()是在父控件上面的高度,即除了手指的移动距离还多了个margin_top的高度。
图示3:这个就显示了,当我们真正使用TranslateAnimation来做动画的时候,它的初始化移动位置,是要比图示2的位置要靠下,因为我们传给TranslateAnimation的TOP值是mRoot.getTop(),而TranslateAnimation的计算的原点不是在父控件的原点,而是控件初始化位置的原点,即多了个margin_top的高度:

 

TranslateAnimation ta = new TranslateAnimation(0, 0, mRoot.getTop(),normal.top);  

如果要修改的话也比较容易,想办法把目的位置改成手指的移动距离就好了;
同样,TranslateAnimation不应该移动到(0,normal.top),因为要移动到初始位置,也就是当前布局左上角所在的(0,0)点,所以修复后的代码应该是:

TranslateAnimation ta = new TranslateAnimation(0, 0, mRoot.getTop()- normal.top, 0);

 

好了,到这里,这篇文章就结束了,大家对于第三部分,讲解网上代码所存在问题的部分,可以忽略,有兴趣的朋友可以仔细看看,感觉讲解起来难度很大啊,也不知道大家能不能看得懂,不行就只有自己根据理解画画出看。

 

源码内容:

1、TryAnimation:第一部分《TranslateAnimation和Layout》对应的源码

2、touchScrollView:第二部分《下拉回弹》对应源码

3、MyScrollView:第三部分《网上解决方案及存在问题》对应源码

 

如果本文有帮到你,记得加关注哦

源码下载地址:http://download.csdn.net/detail/harvic880925/8862361

请大家尊重原创者版权,转载请标明出处: http://blog.csdn.net/harvic880925/article/details/46543859 谢谢

 

如果你喜欢我的文章,你可能更喜欢我的公众号

启舰杂谈

 

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值