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

来源:互联网 发布:js显示display 编辑:程序博客网 时间:2024/05/23 20:14

前言:

相信很多安卓开发者都很头痛的一件事就是公司要求做的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。

@Overrideprotected 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 */@Overrideprotected void onFinishInflate() {if (getChildCount() > 0) {// 获取第一个childviewcontentView = getChildAt(0);}}
好吧,一切准备工作就绪了,就差我们的事件处理了:

// 在触摸事件中处理上拉和下拉的逻辑@Overridepublic 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唯一的一个子viewprivate 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 */@Overrideprotected void onFinishInflate() {if (getChildCount() > 0) {// 获取第一个childviewcontentView = getChildAt(0);}}@Overrideprotected 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());}// 在触摸事件中处理上拉和下拉的逻辑@Overridepublic 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 {@Overrideprotected 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









1 0
原创粉丝点击