android自定义布局中的平滑移动

在android应用程序的开发过程中,相信我们很多人都想把应用的交互做的比较绚丽,比如让界面切换平滑的滚动,还有热度灰常高的伪3D等界面效果,通常情况下,系统提供的应用在特效这方面只能为我们提供简单的动画接口,所以要想实现比较酷炫的效果还是要自己去开发布局控件(即所谓的自定义View、ViewGroup)。小弟也经常做一些自定义的控件,最近工作比较清闲,所以便将自己对自定义布局控件的一些心得写出来,权当是自己的学习笔记了,各位高手看到了可以忽略

。下面就我最近工作中遇到的一个自定义控件开发做一些简单的介绍,其实那个地方原本可以用ScrollView解决很大一部分问题的,但有一些效果确实需要对控件进行重新定义,在继承ScrollView开发中仍然会遇到一些ScrollView自身的限制,所以就仿照ScrollView自己做了一个控件。在其中遇到了一些问题自然就是像ScrollView中拖动的效果(比如快速拖动在手指离开屏幕时控件依旧会由于惯性继续滑动一段距离后才会停止运动),所以就对这个东东做了一下仔细的研究,虽然以前也做过类似的开发,这次由于时间比较充裕,所以将开发中遇到的一些问题都一一记录了下来。下面开始正题:

自定义布局控件自然是要继承某个View或ViewGroup

由于是根据项目的开发来写的这篇博客,所以我就以自定义布局控件(ViewGroup)来做介绍了。

开发一个自定义的ViewGroup自然是要继承ViewGroup类了,在继承这个类之后必须要重写的方法就是

onLayout(boolean changed, int l, int t, int r, int b)

另外至少要有一个构造方法,我个人习惯重写那个有两个参数的构造方法(XXX(Context context, AttributeSet attrs)),因为有了这个构造方法就可以在xml布局文件里使用这个类了。

如果想要对这个布局控件以及其子控件的尺寸进行精确的控制那就要重写下面这个方法了

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

这个方法从字面理解就是估算控件的尺寸大小了,关于这个方法的详细说明引用一下另一位童鞋的文章http://www.eoeandroid.com/thread-102385-1-1.html,这里就不详细介绍了

下面开始介绍关于如何让自定义的控件进行平滑的移动,并能够根据手势的情况产生惯性滑动的效果

先介绍一下开发这种滑动效果需要用到的各种工具类:

android.view.VelocityTracker
android.view.Scroller
android.view.ViewConfiguration

VelocityTracker从字面意思理解那就是速度追踪器了,在滑动效果的开发中通常都是要使用该类计算出当前手势的初始速度(不知道我这么理解是否正确,对应的方法是velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity))并通过getXVelocity或getYVelocity方法得到对应的速度值initialVelocity,并将获得的速度值传递给Scroller类的fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) 方法进行控件滚动时各种位置坐标数值的计算,API中对fling 方法的解释是基于一个fling手势开始滑动动作,滑动的距离将由所获得的初始速度initialVelocity来决定。关于ViewConfiguration 的使用主要使用了该类的下面三个方法:

configuration.getScaledTouchSlop() //获得能够进行手势滑动的距离
configuration.getScaledMinimumFlingVelocity()//获得允许执行一个fling手势动作的最小速度值
configuration.getScaledMaximumFlingVelocity()//获得允许执行一个fling手势动作的最大速度值

需要重写的方法至少要包含下面几个方法:

onTouchEvent(MotionEvent event)//有手势操作必然少不了这个方法了

computeScroll()//必要时由父控件调用请求或通知其一个子节点需要更新它的mScrollX和mScrollY的值。典型的例子就是在一个子节点正在使用Scroller进行滑动动画时将会被执行。所以,从该方法的注释来看,继承这个方法的话一般都会有Scroller对象出现。

在往下就是介绍比较具体的开发思路

首先我们要初始化一些变量,其中的多数代码已经在上面做出介绍了

Java代码

  1. void init(Context context) {
                    mScroller = new Scroller(getContext());
                    setFocusable(true);
                    setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
                    setWillNotDraw(false);
                    final ViewConfiguration configuration = ViewConfiguration.get(context);
                    mTouchSlop = configuration.getScaledTouchSlop();
                    mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
                    mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
            }

复制代码

然后我们申明一个用来处理滑动操作的方法fling(int velocityY),代码如下:

Java代码

  1. public void fling(int velocityY) {
            if (getChildCount() > 0) {
                    mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
                                    maxScrollEdge);
                    final boolean movingDown = velocityY > 0;
                    awakenScrollBars(mScroller.getDuration());
                    invalidate();
            }
    }

复制代码

在这个方法里只是使用Scroller的fling方法开始执行fling手势动作了,关于其中的各种参数就不一一解释了。

awakenScrollBars(int startDelay)方法根据我对注释的理解就是在这里给出动画开始的延时,当参数startDelay为0时动画将立刻开始,其实就是一个延迟的作用

下面是对VelocityTracker的初始化以及资源释放的方法

Java代码

  1. private void obtainVelocityTracker(MotionEvent event) {
            if (mVelocityTracker == null) {
                    mVelocityTracker = VelocityTracker.obtain();
            }
            mVelocityTracker.addMovement(event);
    }
    private void releaseVelocityTracker() {
            if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
            }
    }

复制代码

onTouchEvent(MotionEvent event)方法的重写

Java代码

  1. public boolean onTouchEvent(MotionEvent event) {

  2. if (event.getAction() == MotionEvent.ACTION_DOWN
  3. && event.getEdgeFlags() != 0) {
  4. return false;
  5. }
  6. obtainVelocityTracker(event);
  7. final int action = event.getAction();
  8. final float x = event.getX();
  9. final float y = event.getY();
  10. switch (action) {
  11. case MotionEvent.ACTION_DOWN:
  12. LogUtil.log(TAG, "ACTION_DOWN#currentScrollY:" + getScrollY()
  13. + ", mLastMotionY:" + mLastMotionY,
  14. LogUtil.LOG_E);
  15. if (!mScroller.isFinished()) {
  16. mScroller.abortAnimation();
  17. }
  18. mLastMotionY = y;
  19. break;
  20. case MotionEvent.ACTION_MOVE:
  21. final int deltaY = (int) (mLastMotionY - y);
  22. mLastMotionY = y;
  23. if (deltaY < 0) {
  24. if (getScrollY() > 0) {
  25. scrollBy(0, deltaY);
  26. }
  27. } else if (deltaY > 0) {
  28. mIsInEdge = getScrollY() <= childTotalHeight - height;
  29. if (mIsInEdge) {
  30. scrollBy(0, deltaY);
  31. }
  32. }
  33. break;
  34. case MotionEvent.ACTION_UP:
  35. final VelocityTracker velocityTracker = mVelocityTracker;
  36. velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
  37. int initialVelocity = (int) velocityTracker.getYVelocity();
  38. if ((Math.abs(initialVelocity) > mMinimumVelocity)
  39. && getChildCount() > 0) {
  40. fling(-initialVelocity);
  41. }
  42. releaseVelocityTracker();
  43. break;
  44. }
  45. return true;
  46. }

复制代码

在onTouchEvent方法中,当手势执行到ACTION_UP时获得当时手势的速度值然后判断这个速度值是否大于可滑动的最小速度,如果符合条件那么就执行fling(int velocityY)方法,通过fling方法中的日志发现,在执行了invalidate()方法之后,程序便会执行computeScroll()方法,在computeScroll()方法中执行scrollTo方法主要是因为mScrollX、mScrollY这两个变量的修饰符为portected,无法在扩展类里面无法对这两个变量直接进行操作,那么就需要使用scrollTo方法对这两个变量进行操作,以刷新当前的UI控件,下面附上computeScroll()方法的代码

Java代码

  1. public void computeScroll() {

  2. if (mScroller.computeScrollOffset()) {
  3. int scrollX = getScrollX();
  4. int scrollY = getScrollY();
  5. int oldX = scrollX;
  6. int oldY = scrollY;
  7. int x = mScroller.getCurrX();
  8. int y = mScroller.getCurrY();
  9. scrollX = x;
  10. scrollY = y;
  11. scrollY = scrollY + 10;
  12. scrollTo(scrollX, scrollY);
  13. postInvalidate();
  14. }
  15. }

复制代码

其中的mScroller.computeScrollOffset()是用来判断动画是否完成,如果没有完成返回true继续执行界面刷新的操作,各种位置信息将被重新计算用以重新绘制最新状态的界面。关于scrollTo方法,我们需要看一下该方法的代码(来自View中):

Java代码

  1. public void scrollTo(int x, int y) {

  2. if (mScrollX != x || mScrollY != y) {
  3. int oldX = mScrollX;
  4. int oldY = mScrollY;
  5. mScrollX = x;
  6. mScrollY = y;
  7. onScrollChanged(mScrollX, mScrollY, oldX, oldY);
  8. if (!awakenScrollBars()) {
  9. invalidate();
  10. }
  11. }
  12. }

复制代码

我们可以看到,当传递进来的x、y的值与控件当前的mScrollX、mScrollY的值不相同时对界面进行重新计算,根据日志打印的情况来看似乎awakenScrollBars()返回的总是true, 这样的话每执行一次computeScroll()方法,就需要执行一次postInvalidate()方法来刷新界面,而postInvalidate()方法会通过内部线程重新调用invalidate()已达到界面刷新的效果,产生手势离开屏幕之后的惯性滑动效果。

可能上面说的比较凌乱,在这里总结一下,大概的思路如下:

首先我们通过VelocityTrackerViewConfiguration类得到一些惯性滑动所必须的变量,比如手势离开屏幕时的初始速度,允许进行手势操作的最小距离以及允许手势操作的速度边界值;

第二,创建Scroller的对象,使用它的fling方法供我们控制界面滑动使用;

第三,重写onTouchEvent方法,当我们用手指在屏幕上来回滑动时此时执行的是scrollBy方法来刷新界面,当手指离开屏幕,此时就要开始执行ACTION_UP后面的操作了;

通过对手指离开屏幕时的速度进行判断是否能够进行惯性滑动操作,

如果能够执行那么就使用Scroller类的fling方法启动滑动动画,

这时需要调用一下invalidate()方法来间接的调用computeScroll方法,

在computeScroll方法中对Scroller的动画是否执行完成做了判断,

如果动画没有完成(mScroller.computeScrollOffset() == true)那么就使用scrollTo方法对mScrollX、mScrollY的值进行重新计算刷新界面,

调用postInvalidate()方法重新绘制界面,

postInvalidate()方法会调用invalidate()方法,

invalidate()方法又会调用computeScroll方法,

就这样周而复始的相互调用,直到mScroller.computeScrollOffset() 返回false才会停止界面的重绘动作

总结,滑动效果来看,它依然是在不停的计算控件的位置刷新屏幕,不停的绘制新的图片替换旧的图片,当然每次刷新的速度很快,从而给人一种是在快速滑动的感觉,写到这里我发现,现在所谓的动画总是逃脱不了电影的那种模式,每秒播放多少帧的图片来达到连续播放的效果欺骗人的眼睛。

而且,关于android一些酷炫效果的开发,还是要自己多动手,熟悉View、ViewGroup中每个绘制方法、位置计算方法的调用方式以及顺序,那么至少是在2D动画开发中,也就是一种方式,逃脱不了不停重新绘制的这个圈。

关于熟悉View、ViewGroup中每个绘制方法、位置计算方法的调用方式以及顺序的问题,我建议最好自己写一个简单的自定义View或ViewGroup的扩展类,重载那些绘制、位置计算的方法打个日志出来一看自然就明白了,虽然这个方法很笨,但是很容易出效果的

时间: 2024-10-10 06:45:17

android自定义布局中的平滑移动的相关文章

android 在布局中动态添加控件

第一步 Java代码 final LayoutInflater inflater = LayoutInflater.from(this); 第二步:获取需要被添加控件的布局 Java代码 final LinearLayout lin = (LinearLayout) findViewById(R.id.LinearLayout01); 第三步:获取需要添加的布局(控件) Java代码 LinearLayout layout = (LinearLayout) inflater.inflate( R

android 在布局中合理的使用tag标签的好处

有时候相同的按钮页面的切换,在代码中需要进行多个点击的分开的处理,这些其实是不用这样的操作的,在xml布局中使用tag标签可以很好的处理这些问题:简化操作 布局文件如下: 1 <LinearLayout 2 android:id="@+id/colors" 3 android:layout_width="match_parent" 4 android:layout_height="48dip" 5 android:layout_alignP

Android在布局中动态添加view的两种方法

一.说明 添加视图文件的时候有两种方式:1.通过在xml文件定义layout:2.java代码编写 二.前言说明 1.构造xml文件 2.LayoutInflater 提到addview,首先要了解一下LayoutInflater类.这个类最主要的功能就是实现将xml表述的layout转化为View的功能.为了便于理解,我们可以将它与findViewById()作一比较,二者都是实例化某一对象,不同的是findViewById()是找xml布局文件下的具体widget控件实例化,而LayoutI

Android自定义布局的三种实现方式

在毕设项目中多处用到自定义布局,一直打算总结一下自定义布局的实现方式,今天就来总结一下吧.在此之前学习了郭霖大神博客上面关于自定义View的几篇博文,感觉受益良多,本文中就参考了其中的一些内容. 总结来说,自定义布局的实现有三种方式,分别是:组合控件.自绘控件和继承控件.下面将分别对这三种方式进行介绍. (一)组合控件 组合控件,顾名思义就是将一些小的控件组合起来形成一个新的控件,这些小的控件多是系统自带的控件.比如很多应用中普遍使用的标题栏控件,其实用的就是组合控件,那么下面将通过实现一个简单

Android 自定义布局 性能问题 初探

大家在写android 代码的时候,基本上都使用过如下几种布局 RelativeLayout,LinearLayout, FrameLayout 但是很多时候 这几种布局 也无法满足我们的使用.于是我们会考虑用自定义布局,使用自定义布局会有几个优点 比如可以减少view的使用啊,让ui显示的更加有效率啊,以及实现一些原生控件无法实现的效果. 我们首先去github上 下载一个开源项目 https://github.com/lucasr/android-layout-samples 注意这个项目是

Android自定义布局

1.首先新建属性文件 <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="topbar"> <attr name="title" format="string"/> <attr name="titleTextSize" format=&

从零开始学android开发-布局中 layout_gravity、gravity、orientation、layout_weight

线性布局中,有 4 个及其重要的参数,直接决定元素的布局和位置,这四个参数是 android:layout_gravity ( 是本元素相对于父元素的重力方向 ) android:gravity (是本元素所有子元素的重力方向) android:orientation (线性布局以列或行来显示内部子元素) android:layout_weight (线性布局内子元素对未占用空间[水平或垂直]分配权重值,其值越小,权重越大. 前提是子元素 设置了 android:layout_width = "

在android 自定义listView中绘制矩形

我想在android 在listview中绘制自定义的形状,我在网上找了代码但是没有运行通过,我现在有一个可以绘制矩形的 DrawView.java类,我想在我的自定义listView中展示这个对象. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 DrawView.java package com.example.h

Android 自定义view中的属性,命名空间,以及tools标签

昨日看到有人在知乎上问这3个琐碎的小知识点,今天索性就整理了一下,其实这些知识点并不难,但是很多开发者平时很少注意到这些, 导致的后果就是开发的时候 经常会被ide报错,开发效率很低,或者看开源代码的时候很多地方看不懂. 考虑到现在越来越多的人开发环境迁移到android studio,所以一切以android studio环境为准.和eclipse开发环境相比其实两者是差不多的, 偶有区别 主要也是android studio引入的gradle脚本造成差异. 首先来看看tools标签. 这个地