Android自定义view-弹性ScrollView(上)

前言:

相信很多安卓开发者都很头痛的一件事就是公司要求做的app和IOS端的风格保持一致,很多IOS端中具有弹性的ScrollView在android开发中不失为一个难题(相应的效果见IOS的微信端等等...)对应的软件可以查看。为了实现这样的效果,毫无疑问的一件事:可以考虑重写我们的ScrollView,然后在监听事件中做相应的处理。开题话说的太多了,直接开始我们的教程把。

正文:

首先贴上我们今天要完成的效果图:


看到这边我想大家应该都清楚今天要完成的是什么了吧。其实这个效果实现起来并不困难,重要的是对touch事件的处理,接下来我会给大家提供两套实现方案。一个是用基本的dispatchTouchEvent和TouchEvent去处理事件,另外一个则是用ViewDragHelper去处理view的事件。说实话,个人还是比较推崇第二种方法的,处理事件比较简单,而且还能做出各种想不到的特效。(Tips:对ViewDragHelper不熟悉的可以去百度,在以后的blog中会推出关于ViewDragHelper的讲解).

首先,我还是从难到易来实现吧,这样你才会体会到ViewDragHelper的方便之处。

Step1:继承ScrollVIew,重写ScrollView

public class FlexibleScrollView extends ScrollView {
}
Step2:重写构造器(3中构造,当然何时需要复写哪种构造请自行百度,常识性问题我觉得还是自己去查才能记住 大笑

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

	public FlexibleScrollView(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public FlexibleScrollView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
Step3:因为ScrollView是特殊的Viewgroup,在于它只能有一个childView,一般为LinearLayout来组成它的子view,所以我们需要复写onLayout方法,同时获取我们需要操作的子view。

@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		super.onLayout(changed, l, t, r, b);
		if (contentView == null)
			return;
		// scrollview唯一的一个子view的位置信息,这个位置信息在整个生命周期中保持不变
		originalRect.set(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());

	}
这里的originalRect是我们view的正常位置Rect范围,在整个生命周期中都是保持不变的。

我们的子view是在onFinishInflate() 中获取的,至于onFinishInflate() 什么时候加载,嘿嘿偷笑准备说百度的,算了,提一下吧:

onFinishInflate当View中所有的子控件均被映射成xml后触发,太深奥了?

比如你 自定义一个view叫myView ,路径是,com.test.view.MyView,此view是继承LinearLayout,定义的布局文件是my_view.xml
里面内容是:
<com.test.view.MyView>
        <xxxx />
</com.test.view.MyView>

当你在使用的时候,可以这样使用
MyView mv = (MyView)View.inflate (context,R.layout.my_view,null);
当加载完成xml后,就会执行那个方法。

这段解释也是当时初学安卓的时候百度到的,然后就记下来了。自己太笨了,只能靠记笔记了,当然是在电脑上。安静

好了,看看代码吧:

/**
	 * 在加载完xml后获取唯一的一个childview
	 */
	@Override
	protected void onFinishInflate() {
		if (getChildCount() > 0) {
			// 获取第一个childview
			contentView = getChildAt(0);
		}
	}
好吧,一切准备工作就绪了,就差我们的事件处理了:

// 在触摸事件中处理上拉和下拉的逻辑
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		if (contentView == null) {
			return super.dispatchTouchEvent(ev);
		}
		int action = ev.getAction();
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			// 判断是否可以上拉或者下拉
			canPullDown = isCanPullDown();
			canPullUp = isCanPullUp();
			// 记录按下时的Y值
			startY = ev.getY();
			break;
		case MotionEvent.ACTION_UP:
			if (!isMoved)
				break; // 如果没有移动布局,则跳过执行
			// 开启动画
			TranslateAnimation anim = new TranslateAnimation(0, 0, contentView.getTop(), originalRect.top);
			// 设置动画时间
			anim.setDuration(ANIM_TIME);
			// 给view设置动画
			contentView.setAnimation(anim);
			// 设置回到正常的布局位置
			contentView.layout(originalRect.left, originalRect.top, originalRect.right, originalRect.bottom);
			// 将标志位重置
			canPullDown = false;
			canPullUp = false;
			isMoved = false;
			break;
		case MotionEvent.ACTION_MOVE:
			// 在移动过程既没有达到上拉的程度,又没有达到下拉的程度
			if (!canPullDown && !canPullUp) {
				startY = ev.getY();
				canPullDown = isCanPullDown();
				canPullUp = isCanPullUp();
				break;
			}
			// 计算手指移动的距离
			float nowY = ev.getY();
			int deltaY = (int) (nowY - startY);
			// 是否应该移动布局
			// 1.可以下拉,并且手指向下移动
			// 2.可以上拉,并且手指向上移动
			// 3.既可以上拉也可以下拉,当ScrollView包裹的控件比scrollView还小
			boolean shouldMove = (canPullDown && deltaY > 0) || (canPullUp && deltaY < 0) || (canPullDown && canPullUp);
			if (shouldMove) {
				// 计算偏移量
				int offset = (int) (deltaY * MOVE_FACTOR);
				contentView.layout(originalRect.left, originalRect.top + offset, originalRect.right, originalRect.bottom + offset);
				isMoved = true;
			}
			break;
		}
		return super.dispatchTouchEvent(ev);
	}

	/**
	 * 判断是否滚动到顶部
	 * 
	 * @return
	 */
	private boolean isCanPullDown() {
		return getScrollY() == 0 || contentView.getHeight() < getHeight() + getScrollY();
	}

	/**
	 * 判断是否滚动到底部
	 */
	private boolean isCanPullUp() {
		return contentView.getHeight() <= getScrollY() + getHeight();
	}

这段代码有点长,分别是判断我们手指按下,手指移动和手指抬起时view的位置变化来重新onLayout。上面都有注解,相信不用花太多时间就能看懂。好了,整体代码我就贴上去吧。 

activity_main.xml:

<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" >

    <com.beyole.view.FlexibleScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical" >

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="200.0dip"
                android:scaleType="fitXY"
                android:src="@drawable/img1" />

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="200.0dip"
                android:scaleType="fitXY"
                android:src="@drawable/img2" />

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="200.0dip"
                android:scaleType="fitXY"
                android:src="@drawable/img3" />
        </LinearLayout>
    </com.beyole.view.FlexibleScrollView>

</RelativeLayout>

就是简单的调用,和ScrollView一样。

FlexibleScrollView.java:

package com.beyole.view;

import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.ScrollView;

/**
 * 有弹性的scrollView
 * 
 * @date 2016/02/22
 * @author Iceberg
 * 
 */
public class FlexibleScrollView extends ScrollView {

	private static final String TAG = "FLEXIBLESCROLLVIEW";
	// 移动因子,是一个百分比,比如手指移动了100px,那么view只移动50px,目的是达到一个延迟的效果。
	private static final float MOVE_FACTOR = 0.5f;
	// 手指松开时,界面回到原始位置动画所需的时间
	private static final int ANIM_TIME = 300;
	// ScrollView唯一的一个子view
	private View contentView;
	// 手指按下时的Y值,用于计算移动中的移动距离
	// 如果按下时不能上拉或者下拉,会在手指移动时更新为当前手指的Y值。
	private float startY;
	// 用于记录正常的布局位置
	private Rect originalRect = new Rect();
	// 记录手指按下时是否可以下拉
	private boolean canPullDown = false;
	// 记录手指按下时是否可以上拉
	private boolean canPullUp = false;
	// 在手指滑动时的过程中记录是否移动了布局
	private boolean isMoved = false;

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

	public FlexibleScrollView(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public FlexibleScrollView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}

	/**
	 * 在加载完xml后获取唯一的一个childview
	 */
	@Override
	protected void onFinishInflate() {
		if (getChildCount() > 0) {
			// 获取第一个childview
			contentView = getChildAt(0);
		}
	}

	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		super.onLayout(changed, l, t, r, b);
		if (contentView == null)
			return;
		// scrollview唯一的一个子view的位置信息,这个位置信息在整个生命周期中保持不变
		originalRect.set(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());

	}

	// 在触摸事件中处理上拉和下拉的逻辑
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		if (contentView == null) {
			return super.dispatchTouchEvent(ev);
		}
		int action = ev.getAction();
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			// 判断是否可以上拉或者下拉
			canPullDown = isCanPullDown();
			canPullUp = isCanPullUp();
			// 记录按下时的Y值
			startY = ev.getY();
			break;
		case MotionEvent.ACTION_UP:
			if (!isMoved)
				break; // 如果没有移动布局,则跳过执行
			// 开启动画
			TranslateAnimation anim = new TranslateAnimation(0, 0, contentView.getTop(), originalRect.top);
			// 设置动画时间
			anim.setDuration(ANIM_TIME);
			// 给view设置动画
			contentView.setAnimation(anim);
			// 设置回到正常的布局位置
			contentView.layout(originalRect.left, originalRect.top, originalRect.right, originalRect.bottom);
			// 将标志位重置
			canPullDown = false;
			canPullUp = false;
			isMoved = false;
			break;
		case MotionEvent.ACTION_MOVE:
			// 在移动过程既没有达到上拉的程度,又没有达到下拉的程度
			if (!canPullDown && !canPullUp) {
				startY = ev.getY();
				canPullDown = isCanPullDown();
				canPullUp = isCanPullUp();
				break;
			}
			// 计算手指移动的距离
			float nowY = ev.getY();
			int deltaY = (int) (nowY - startY);
			// 是否应该移动布局
			// 1.可以下拉,并且手指向下移动
			// 2.可以上拉,并且手指向上移动
			// 3.既可以上拉也可以下拉,当ScrollView包裹的控件比scrollView还小
			boolean shouldMove = (canPullDown && deltaY > 0) || (canPullUp && deltaY < 0) || (canPullDown && canPullUp);
			if (shouldMove) {
				// 计算偏移量
				int offset = (int) (deltaY * MOVE_FACTOR);
				contentView.layout(originalRect.left, originalRect.top + offset, originalRect.right, originalRect.bottom + offset);
				isMoved = true;
			}
			break;
		}
		return super.dispatchTouchEvent(ev);
	}

	/**
	 * 判断是否滚动到顶部
	 * 
	 * @return
	 */
	private boolean isCanPullDown() {
		return getScrollY() == 0 || contentView.getHeight() < getHeight() + getScrollY();
	}

	/**
	 * 判断是否滚动到底部
	 */
	private boolean isCanPullUp() {
		return contentView.getHeight() <= getScrollY() + getHeight();
	}
}
MainActivity.java:

package com.beyole.flexiblescrollview;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

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

}
由于篇幅问题,第一种方法到此就结束了,其实整个逻辑不难理解。第二种用ViewDragHelper实现的,请移步: http://blog.csdn.net/smarticeberg/article/details/50717963

下载地址:http://download.csdn.net/detail/smarticeberg/9439380

Github地址:https://github.com/xuejiawei/beyole_FlexibleScrollView,欢迎fork or star


题外话:

android交流群:279031247(广告勿入)

新浪微博:SmartIceberg









  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值