Android自定义View之常用工具源码分析

从开始接触Android开发到现在也不敢三个多月的时间,身为学生,又担任部长,加上各种学校活动,并没有太多的时间去学习Android,但正如我前面的一篇文章我的2016踩坑之旅所说的那样,学会合理安排时间,利用时间是非常重要的,对科学技术保持好奇心也是非常必要的。于是乎,就这样,每天八点起床,到晚上十点,在除去各种工作和活动之外,便都是我的学习时间,我觉得时间还是挺多的,这三个月边学边做,终于在十二月初完成了第一个项目,现在终于有时间来总结并开始新的学习了。

Android自定义View让很多人都感到头疼,因为太难了。我也这么觉得,但是学会自定义View的好处不言而喻,除了自定义View外,其他的形如自定义事件等,还不是相同的道理吗?更重要的是,一个好的产品也需要一个好的包装,APP也是如此,一个优秀的APP需要一个炫酷、优质的UI界面和封装。而且,在我看来,一个应用程序,他的总体框架就是UI界面,只要通过自定义View把这框架搭了起来,剩下的就是在这个框架上添砖加瓦了。所以,我决定开始我的自定义View学习之旅,希望和大家一起学习,一起进步。

踏上这条学习之路,最要感谢的是我的师兄,很多东西我都是从他口中得知,他的技术也很好,是我心中永远的大神,下面放师兄的博客。

无比耿直的程序猿


好了,接下来进入主题,开始我的自定义View学习之旅。就在我决定要学习自定义View后,刚好看到了 Android多分辨率适配框架(3)— 使用指南这一篇文章,看名字很不错嘛,当然事实上也很不错,而且非常详细,从源码角度分析,直指本质啊!刚好我之前也遇到过分辨率适配问题,就看一下,结果把作者关于适配器的三篇文章都看完了。其实最重要的是第一次看的时候就发现了作者有一系列的自定义View教程和文章,看完这三篇就知道作者的文章都不错,于是暗中决定就从此开始我的自定义View学习之旅吧。

常用工具介绍

在使用自定义View的时候,常常会用到一些Android系统提供的工具。这些工具封装了我们经常会用到的方法,比如拖拽View,计算滑动速度,View的滚动,手势处理等等。如果我们自己去实现这些方法会比较繁琐,而且容易出一些bug。所以,作为自定义View系列学习和教程的开端,先了解一下这些常用的工具,以便在后续的学习和工作中使用。

  • Configuration

  • ViewConfiguration

  • GestureDetector

  • VelocityTracker

  • Scroller

  • ViewDragHelper

现在这些工具都在这了,接下来就让我们来一个个分析吧


Configuration

Gogle官方文档对Configuration的描述如下:

This class describes all device configuration information that can impact the resources the application retrieves. This includes both user-specified configuration options (locale list and scaling) as well as device configurations (such as input modes, screen size and screen orientation).

You can acquire this object from Resources, using getConfiguration(). Thus, from an activity, you can get it by chaining the request with getResources():

意思是你可以通过使用getConfiguration()方法从Resources获取此对象。因此,在一个Activity中,你可以通过用getResources()方法请求来获得它:

Configuration config = getResources().getConfiguration();

Configuration用来描述设备的配置信息。
比如用户的配置信息:locale和scaling等等
比如设备的相关信息:输入模式,屏幕大小, 屏幕方向等等

我们可以通过如下方式来获取需要的相关信息:

Configuration configuration = getResources().getConfiguration();
//获取国家码
int countryCode = configuration.mcc;
//获取网络码
int networkCode = configuration.mnc;
//判断横竖屏
if(configuration.orientation == Configuration.ORIENTATION_PORTRAIT){

   } else {

}

ViewConfiguration

看完Configuration再来看看ViewConfiguration。这两者的名字有些像,差了一个View;咋一看,还以为它们是继承关系呢,其实不然。

来看一下Google官方文档对于ViewConfiguration的描述:

Contains methods to standard constants used in the UI for timeouts, sizes, and distances.

意思是包含在UI中用于超时,大小和距离的标准常量的方法。
ViewConfiguration提供了一些自定义控件用到的标准常量,比如尺寸大小,滑动距离,敏感度等等。

可以利用ViewConfiguration的静态方法获取一个实例

ViewConfiguration viewConfiguration = ViewConfiguration.get(context);

这里介绍一下ViewConfiguration的几个对象方法

ViewConfiguration  viewConfiguration = ViewConfiguration.get(context);
//获取touchSlop。该值表示系统所能识别出的被认为是滑动的最小距离
int touchSlop = viewConfiguration.getScaledTouchSlop();
//获取Fling速度的最小值和最大值
int minimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
int maximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
//判断是否有物理按键
boolean isHavePermanentMenuKey = viewConfiguration.hasPermanentMenuKey();

ViewConfiguration还提供了一些非常有用的静态方法,比如:

//双击间隔时间.在该时间内是双击,否则是单击
int doubleTapTimeout = ViewConfiguration.getDoubleTapTimeout();
//按住状态转变为长按状态需要的时间
int longPressTimeout = ViewConfiguration.getLongPressTimeout();
//重复按键的时间
int keyRepeatTimeout = ViewConfiguration.getKeyRepeatTimeout();
//以毫秒为单位的持续时间,我们将等待以查看触摸事件是否是跳转点击。如果用户在此间隔内不移动,则认为是轻敲。
int jumpTapTimeout = ViewConfiguration.getJumpTapTimeout();

GestureDetector

老规矩,先看一下Google官方文档对GestureDetector的描述

Detects various gestures and events using the supplied MotionEvents. The GestureDetector.OnGestureListener callback will notify users when a particular motion event has occurred. This class should only be used with MotionEvents reported via touch (don’t use for trackball events). To use this class:

上面的大体意思是:

使用GestureDetector 提供的MotionEvents检测各种手势和事件。GestureDetector.OnGestureListener回调将在特定运事件发生时通知用户。此类只应与通过触摸报告的MotionEvent(不用于轨迹球事件)一起使用。要使用此类,需要完成以下几点:

  • Create an instance of the GestureDetector for your View

  • In the onTouchEvent(MotionEvent) method ensure you call onTouchEvent(MotionEvent). The methods defined in your callback will be executed when the events occur.

  • If listening for onContextClick(MotionEvent) you must call onGenericMotionEvent(MotionEvent) in onGenericMotionEvent(MotionEvent).

意思是:

  • 为您的视图创建GestureDetector的实例

  • 在onTouchEvent(MotionEvent)方法中,确保调用onTouchEvent(MotionEvent)方法。在回调中定义的方法将在事件发生时执行。

  • 如果监听onContextClick(MotionEvent),则必须在onGenericMotionEvent(MotionEvent)中调用onGenericMotionEvent(MotionEvent)方法。

我们都知道,我们可以在onTouchEvent()中自己处理手势。其实Android系统也给我们提供了一个手势处理的工具,这就是GestureDetector手势监听类。利用GestureDetector可以简化许多操作,轻松实现一些常用的功能。

那么接下来就让我们来看一下它是如何使用的吧!

第一步:实现OnGestureListener

private class GestureListenerImpl implements GestureDetector.OnGestureListener {

        //触摸屏幕时均会调用该方法
        @Override
        public boolean onDown(MotionEvent e) {
            System.out.println("---> 手势中的onDown方法");
            return false;
        }

        //手指在屏幕上拖动时会调用该方法
        @Override
        public boolean onFling(MotionEvent e1,MotionEvent e2, float velocityX,float velocityY) {
            System.out.println("---> 手势中的onFling方法");
            return false;
        }

        //手指长按屏幕时均会调用该方法
        @Override
        public void onLongPress(MotionEvent e) {
            System.out.println("---> 手势中的onLongPress方法");
        }

        //手指在屏幕上滚动时会调用该方法
        @Override
        public boolean onScroll(MotionEvent e1,MotionEvent e2, float distanceX,float distanceY) {
            System.out.println("---> 手势中的onScroll方法");
            return false;
        }

        //手指在屏幕上按下,且未移动和松开时调用该方法
        @Override
        public void onShowPress(MotionEvent e) {
            System.out.println("---> 手势中的onShowPress方法");
        }

        //轻击屏幕时调用该方法
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            System.out.println("---> 手势中的onSingleTapUp方法");
            return false;
        }
    }

第二步:生成GestureDetector对象

GestureDetector gestureDetector = new GestureDetector(context,new GestureListenerImpl());

这里的GestureListenerImpl就是GestureListener监听器的实现。

第三步:将Touch事件交给GestureDetector处理

比如将Activity的Touch事件交给GestureDetector处理

@Override  
public boolean onTouchEvent(MotionEvent event) {  
     return mGestureDetector.onTouchEvent(event);  
}

比如将View的Touch事件交给GestureDetector处理

mButton = (Button) findViewById(R.id.button);  
mButton.setOnTouchListener(new OnTouchListener() {            
   @Override  
   public boolean onTouch(View arg0, MotionEvent event) {  
          return mGestureDetector.onTouchEvent(event);  
      }  
});  

VelocityTracker

还是老规矩,看一下Google官方文档对VelocityTracker的描述

Helper for tracking the velocity of touch events, for implementing flinging and other such gestures. Use obtain() to retrieve a new instance of the class when you are going to begin tracking. Put the motion events you receive into it with addMovement(MotionEvent). When you want to determine the velocity call computeCurrentVelocity(int) and then call getXVelocity(int) and getYVelocity(int) to retrieve the velocity for each pointer id.

大概意思就是:

这是一个帮助器,用于跟踪触摸事件的速度,用于实现拖拽和其他这样的手势。当您要开始跟踪时,使用gets()来检索类的新实例。使用addMovement(MotionEvent)将接收的运动事件放入其中。当你想要确定速度调用computeCurrentVelocity(int),然后调用getXVelocity(int)和getYVelocity(int)检索每个指针id的速度。

其实这个工具一看名字,就很容易猜到意思了啊,速度追踪嘛。

VelocityTracker用于跟踪触摸屏事件(比如,Flinging及其他Gestures手势事件等)的速率。

简单说一下它的常用套路。

第一步:开始速度追踪

private void startVelocityTracker(MotionEvent event) {  
    if (mVelocityTracker == null) {  
         mVelocityTracker = VelocityTracker.obtain();  
     }  
     mVelocityTracker.addMovement(event);  
}  

在这里我们初始化VelocityTracker,并且把要追踪的MotionEvent注册到VelocityTracker的监听中。

第二步:获取追踪到的速度

private int getScrollVelocity() {  
     // 设置VelocityTracker单位.1000表示1秒时间内运动的像素  
     mVelocityTracker.computeCurrentVelocity(1000);  
     // 获取在1秒内X方向所滑动像素值  
     int xVelocity = (int) mVelocityTracker.getXVelocity();  
     return Math.abs(xVelocity);  
    }

获取1秒内Y方向所滑动像素值的原理同上

第三步:解除速度追踪

private void stopVelocityTracker() {  
      if (mVelocityTracker != null) {  
          mVelocityTracker.recycle();  
          mVelocityTracker = null;  
      }  
}  

Scroller

老规矩,看一下Google官方文档对Scroller的描述

This class encapsulates scrolling. You can use scrollers (Scroller or OverScroller) to collect the data you need to produce a scrolling animation—for example, in response to a fling gesture. Scrollers track scroll offsets for you over time, but they don’t automatically apply those positions to your view.

It’s your responsibility to get and apply new coordinates at a rate that will make the scrolling animation look smooth.

大概意思是:

这个类封装了滚动。您可以使用滚动器(滚动器或OverScroller)来收集生成滚动动画所需的数据,例如,响应fling手势。滚动条跟踪您的滚动偏移量,但它们不会自动应用这些位置到您的视图。

获得和应用新的坐标,将使滚动动画看起来拥有流畅的速度是你的责任。

举个例子:

 private Scroller mScroller = new Scroller(context);
 ...
 public void zoomIn() {
     // Revert any animation currently in progress
     mScroller.forceFinished(true);
     // Start scrolling by providing a starting point and
     // the distance to travel
     mScroller.startScroll(0, 0, 100, 0);
     // Invalidate to request a redraw
     invalidate();
 }

如果想要跟踪x / y坐标的更改位置,可以使用computeScrollOffset()方法。该方法返回一个布尔值以指示滚动器是否完成。如果不是,则意味着fling或编程泛操作仍在进行中。你可以使用此方法查找x和y坐标的当前偏移量,例如:

if (mScroller.computeScrollOffset()) {
     // Get current x and y positions
     int currX = mScroller.getCurrX();
     int currY = mScroller.getCurrY();
    ...
 }

相信大家对Scroller也比较熟悉,所以这里我们也不讲太多,只强调几点:

第一点:scrollTo()和scrollBy()的关系

先看scrollBy( )的源码

public void scrollBy(int x, int y) {   
       scrollTo(mScrollX + x, mScrollY + y);   
}

很清晰嘛,也就是说scrollBy( )方法调用了scrollTo( )方法,最终起作用的是scrollTo( )方法。

第二点:scroll的本质
scrollTo( )和scrollBy( )移动的只是View的内容,而且View的背景是不移动的。

第三点:scrollTo( )和scrollBy( )方法的坐标说明

假设我们对于一个TextView调用scrollTo(0,25) ;那么该TextView中的content(比如显示的文字:Hello)会怎么移动呢?

向下移动25个单位?不!恰好相反!!这是为什么呢?
因为调用该方法会导致视图重绘,即会调用

public void invalidate(int l, int t, int r, int b)

此处的l,t,r,b四个参数表示View原来的坐标

在该方法中最终会调用:

tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY);
p.invalidateChild(this, tmpr);

其中tmpr是一个Rect,this是原来的View;通过这两行代码就把View在一个Rect中重绘。
请注意第一行代码:
原来的l和r均减去了scrollX
原来的t和b均减去了scrollY
也就是说scrollX如果是正值,那么重绘后的View的宽度反而减少了;反之同理
也就是说scrollY如果是正值,那么重绘后的View的高度反而减少了;反之同理
所以,TextView调用scrollTo(0,25)和我们的理解相反

scrollBy(int x,int y)方法与上类似,不再多说了.


ViewDragHelper

老规矩,看一下Google官方文档对ViewDragHelper 的描述

ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number of useful operations and state tracking for allowing a user to drag and reposition views within their parent ViewGroup.

大概意思是:

ViewDragHelper是一个用于编写自定义ViewGroups的实用程序类。它提供了许多有用的操作和状态跟踪,以允许用户在其父ViewGroup中拖动和重新定位视图。

在项目中很多场景需要用户手指拖动其内部的某个View,此时就需要在onInterceptTouchEvent()和onTouchEvent()这两个方法中写不少逻辑了,比如处理:拖拽移动,越界,多手指的按下,加速度检测等等。

ViewDragHelper可以极大的帮我们简化类似的处理,它提供了一系列用于处理用户拖拽子View的辅助方法和与其相关的状态记录。比较常见的:QQ侧滑菜单,Navigation Drawer的边缘滑动,都可以由它实现。

ViewDragHelper的使用并不复杂,在此通过一个示例展示其常用的用法。

public class MyLinearLayout extends LinearLayout {
    private ViewDragHelper mViewDragHelper;

    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initViewDragHelper();
    }

    //初始化ViewDragHelper
    private void initViewDragHelper() {
        mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                return true;
            }

            //处理水平方向的越界
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                int fixedLeft;
                View parent = (View) child.getParent();
                int leftBound = parent.getPaddingLeft();
                int rightBound = parent.getWidth() - child.getWidth() - parent.getPaddingRight();

                if (left < leftBound) {
                    fixedLeft = leftBound;
                } else if (left > rightBound) {
                    fixedLeft = rightBound;
                } else {
                    fixedLeft = left;
                }
                return fixedLeft;
            }

            //处理垂直方向的越界
            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                int fixedTop;
                View parent = (View) child.getParent();
                int topBound = getPaddingTop();
                int bottomBound = getHeight() - child.getHeight() - parent.getPaddingBottom();
                if (top < topBound) {
                    fixedTop = topBound;
                } else if (top > bottomBound) {
                    fixedTop = bottomBound;
                } else {
                    fixedTop = top;
                }
                return fixedTop;
            }

            //监听拖动状态的改变
            @Override
            public void onViewDragStateChanged(int state) {
                super.onViewDragStateChanged(state);
                switch (state) {
                    case ViewDragHelper.STATE_DRAGGING:
                        System.out.println("STATE_DRAGGING");
                        break;
                    case ViewDragHelper.STATE_IDLE:
                        System.out.println("STATE_IDLE");
                        break;
                    case ViewDragHelper.STATE_SETTLING:
                        System.out.println("STATE_SETTLING");
                        break;
                }
            }

            //捕获View
            @Override
            public void onViewCaptured(View capturedChild, int activePointerId) {
                super.onViewCaptured(capturedChild, activePointerId);
                System.out.println("ViewCaptured");
            }

            //释放View
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                super.onViewReleased(releasedChild, xvel, yvel);
                System.out.println("ViewReleased");
            }
        });
    }

    //将事件拦截交给ViewDragHelper处理
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    //将Touch事件交给ViewDragHelper处理
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mViewDragHelper.processTouchEvent(ev);
        return true;
    }
}

从这个例子可以看出来ViewDragHelper是作用在ViewGroup上的(比如LinearLayout)而不是直接作用到某个被拖拽的子View。其实这也不难理解,因为子View在布局中的位置是其所在的ViewGroup决定的。

在该例中ViewDragHelper做了如下主要操作:

  • (1) ViewDragHelper接管了ViewGroup的事件拦截,代码第86-89行

  • (2) ViewDragHelper接管了ViewGroup的Touch事件,代码第92-96行

  • (3) ViewDragHelper处理了拖拽子View时的边界越界,代码第17-50行

  • (4) ViewDragHelper监听拖拽子View时的状态变化,代码第53-67行

除了这些常见的操作,ViewDragHelper还可以实现:抽屉拉伸,拖拽结束松手后子View自动返回到原位等复杂操作。


好了,Android自定义View需要用到的工具我们都介绍了一遍,接下来就是开始正式学习了,可能下一篇时间会有点晚,因为我也还需要去熟悉这些工具。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值