---------------------------------------------------------------------------
Android自定义View学习 Android自定义View学习教程——01
---------------------------------------------------------------------------
从开始接触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
---------------------------------------------------------------------------
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)方法。
---------------------------------------------------------------------------
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行
---------------------------------------------------------------------------
没有评论:
发表评论