自定义view

来源:互联网 发布:linux远程控制mingling 编辑:程序博客网 时间:2024/06/08 11:26

  面试的时候总是被问到怎么去自定义view,而我回答的总是三点说重写onMeasure()、onLayout()和onDraw().其中onMeasure()是决定view的大小,onLayout()决定view的位置,onDraw()就是具体的绘制view。其实这些说的都没错,只是说的太笼统太简单了,今天我就以郭神的带你一步步深入了解View为基调,说说我重新认识的自定义view。
  
1,Android LayoutInflater原理分析。
  相信大家对于LayoutInflater都不陌生,知道是用来加载布局的。对于用来加载布局,大家最熟悉的莫过于在每个Activity中onCreate()里面setContentView(),其实setContentView()里面的加载布局的实现也是用到了LayoutInflater啦。
  先看一下LayoutInflater的基本使用方法,首先需要获得它的实例,这有两种方式,第一种方式如下:
   LayoutInflater mLayoutInflater = LayoutInflater.from(context);

第二种方式如下:

 LayoutInflater mLayoutInflater =(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);   

  其实第一种就是第二种的简单写法,只是Android给我们做了一下封装而已。得到了LayoutInflater的实例之后就可以调用它的inflate()方法来加载布局了,如下所示:
  

mLayoutInflater.inflate(resourceId, root)

inflate方法里面的两个参数:resourceId是布局的Id,root是是指给该布局的外部再嵌套一层父布局,如果不需要就直接传null。

举例子:
activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:id="@+id/main_layout"      android:layout_width="match_parent"      android:layout_height="match_parent" >  </LinearLayout>  

button_layout.xml

<Button xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="wrap_content"      android:layout_height="wrap_content"      android:text="Button" >  </Button>  

如何通过LayoutInflater来将button_layout这个布局添加到主布局文件的LinearLayout中。根据刚刚介绍的用法,修改MainActivity中的代码,如下所示:

public class MainActivity extends Activity {      private LinearLayout mainLayout;      @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);          mainLayout = (LinearLayout) findViewById(R.id.main_layout);          LayoutInflater layoutInflater = LayoutInflater.from(this);          View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null);          mainLayout.addView(buttonLayout);      }  }  

可以看到,这里先是获取到了LayoutInflater的实例,然后调用它的inflate()方法来加载button_layout这个布局,最后调用LinearLayout的addView()方法将它添加到LinearLayout中。

LayoutInflater其实就是使用Android提供的pull解析方式来解析布局文件的。
  首先会调用createViewFromTag()这个方法来获取View的实例。看到这个方法名,我们就应该能猜到,它是用于根据节点名来创建View对象的。其实确实如此,它需要把节点名和参数传进去然后在其内部又会去调用createView()方法,最后使用反射的方式创建出View的实例并返回。
  当然,这里只是创建出了一个根布局的实例而已,接下来会调用rInflate()方法来循环遍历这个根布局下的子元素,每次递归完成后则将这个View添加到父布局当中。
  这样的话,把整个布局文件都解析完成后就形成了一个完整的DOM结构,最终会把最顶层的根布局返回,至此inflate()过程全部结束。
小知识点提示:
  在setContentView()方法中,Android会自动在布局文件的最外层再嵌套一个FrameLayout,所以layout_width和layout_height属性才会有效果。
  任何一个Activity中显示的界面其实主要都由两部分组成,标题栏和内容布局。标题栏就是在很多界面顶部显示的那部分内容,可以在代码中控制让它是否显示。而内容布局就是一个FrameLayout,这个布局的id叫作content,我们调用setContentView()方法时所传入的布局其实就是放到这个FrameLayout中的,这也是为什么这个方法名叫作setContentView(),而不是叫setView()。

2,Android视图绘制流程完全解析。
  任何一个视图都不可能凭空突然出现在屏幕上,它们都是要经过非常科学的绘制流程后才能显示出来的。每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw(),下面我们逐个对这三个阶段展开进行探讨。
1,onMeasure()分析
  View系统的绘制流程会从ViewRoot的performTraversals()[执行遍历]方法中开始,在其内部调用View的measure()方法。measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,如下所示:

  1. EXACTLY
    表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
  2. AT_MOST
    表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
  3. UNSPECIFIED
    表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

那么你可能会有疑问了,widthMeasureSpec和heightMeasureSpec这两个值又是从哪里得到的呢?通常情况下,这两个值都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。但是最外层的根视图,它的widthMeasureSpec和heightMeasureSpec又是从哪里得到的呢?这就需要去分析ViewRoot中的源码了,观察performTraversals()方法可以发现根视图总是会充满全屏的。

介绍了这么多MeasureSpec相关的内容,接下来我们了解下View的measure()方法吧。
  注意观察,measure()这个方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架的。然后在其里面调用了onMeasure()方法,这里才是真正去测量并设置View大小的地方,默认会调用getDefaultSize()方法来获取视图的大小,这里传入的measureSpec是一直从measure()方法中传递过来的。然后调用MeasureSpec.getMode()方法可以解析出specMode,调用MeasureSpec.getSize()方法可以解析出specSize。接下来进行判断,如果specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的行为。之后会在onMeasure()方法中调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。
  当然,一个界面的展示可能会涉及到很多次的measure,因为一个布局中一般都会包含多个子视图,每个视图都需要经历一次measure过程。ViewGroup中定义了一个measureChildren()方法来去测量子视图的大小,分别调用了getChildMeasureSpec()方法来去计算子视图的MeasureSpec,然后再调用子视图的measure()方法,并把计算出的MeasureSpec传递进去,之后的流程就和前面所介绍的一样了。
当然,onMeasure()方法是可以重写的,也就是说,如果你不想使用系统默认的测量方式,可以按照自己的意愿进行定制:

 public class MyView extends View {      ......      @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          setMeasuredDimension(200, 200);  //设置测量尺寸。这样的话就把View默认的测量流程覆盖掉了,不管在布局文件中定义MyView这个视图的大小是多少,最终在界面上显示的大小都将会是200*200。    }  }     

需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。

由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。

到此为止,我们就把视图绘制流程的第一阶段分析完了。

2,onLayout()分析
  measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程,如下所示:

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight); // host.mMeasuredWidth和host.mMeasuredHeight就是在setMeasuredDimension()方法调用之后,我们使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高。

layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。

在layout()方法中,首先会调用setFrame()方法来判断视图的大小是否发生过变化,以确定有没有必要对当前的视图进行重绘,同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。

接下来会像在measure()里面调用onMeasure()方法一样,这个layout()方法会在其里面调用onLayout()方法。进入onLayout()方法,咦?怎么这是个空方法,一行代码都没有?!

没错,View中的onLayout()方法就是一个空方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。既然如此,我们来看下ViewGroup(父视图)中的onLayout()方法是怎么写的吧,代码如下:

@Override  protected abstract void onLayout(boolean changed, int l, int t, int r, int b);  

可以看到,ViewGroup中的onLayout()方法竟然是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。没错,像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。由于LinearLayout和RelativeLayout的布局规则都比较复杂,就不单独拿出来进行分析了,这里我们尝试自定义一个布局,借此来更深刻地理解onLayout()的过程。

自定义的这个布局目标很简单,只要能够包含一个子视图,并且让子视图正常显示出来就可以了。那么就给这个布局起名叫做SimpleLayout吧,代码如下所示:

public class SimpleLayout extends ViewGroup{ public SimpleLayout(Context context, AttributeSet attrs) {          super(context, attrs);      }  @overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){super.onMeasure(widthMeasureSpec, heightMeasureSpec);          if (getChildCount() > 0) {              View childView = getChildAt(0);              measureChild(childView, widthMeasureSpec, heightMeasureSpec);          }  }@overrideprotected void onLayout(boolean changed, int l, int t, int r, int b){  if (getChildCount() > 0) {              View childView = getChildAt(0);              childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());          }  }}

onMeasure()方法会在onLayout()方法之前调用,因此这里在onMeasure()方法中判断SimpleLayout中是否有包含一个子视图,如果有的话就调用measureChild()方法来测量出子视图的大小。

接着在onLayout()方法中同样判断SimpleLayout是否有包含一个子视图,然后调用这个子视图的layout()方法来确定它在SimpleLayout布局中的位置,这里传入的四个参数依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分别代表着子视图在SimpleLayout中左上右下四个点的坐标。其中,调用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中测量出的宽和高。

这样就已经把SimpleLayout这个布局定义好了,下面就是在XML文件中使用它了,如下所示:

<com.example.viewtest.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="match_parent"      android:layout_height="match_parent" >      <ImageView           android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:src="@drawable/ic_launcher"          />  </com.example.viewtest.SimpleLayout>  

OK!ImageView成功已经显示出来了,并且显示的位置也正是我们所期望的。如果你想改变ImageView显示的位置,只需要改变childView.layout()方法的四个参数就行了。

  在onLayout()过程结束后,我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了。说到这里,我相信很多朋友长久以来都会有一个疑问,getWidth()方法和getMeasureWidth()方法到底有什么区别呢?它们的值好像永远都是相同的。其实它们的值之所以会相同基本都是因为布局设计者的编码习惯非常好,实际上它们之间的差别还是挺大的。

  首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
  观察SimpleLayout中onLayout()方法的代码,这里给子视图的layout()方法传入的四个参数分别是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),因此getWidth()方法得到的值就是childView.getMeasuredWidth() - 0 = childView.getMeasuredWidth() ,所以此时getWidth()方法和getMeasuredWidth() 得到的值就是相同的,但如果你将onLayout()方法中的代码进行如下修改:
  

@Override  protected void onLayout(boolean changed, int l, int t, int r, int b) {      if (getChildCount() > 0) {          View childView = getChildAt(0);          childView.layout(0, 0, 200, 200);      }  }  

这样getWidth()方法得到的值就是200 - 0 = 200,不会再和getMeasuredWidth()的值相同了。当然这种做法充分不尊重measure()过程计算出的结果,通常情况下是不推荐这么写的。getHeight()与getMeasureHeight()方法之间的关系同上,就不再重复分析了。

到此为止,我们把视图绘制流程的第二阶段也分析完了。

3. onDraw()分析

  measure和layout的过程都结束后,接下来就进入到draw的过程了。同样,根据名字你就能够判断出,在这里才真正地开始对视图进行绘制。ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作。
draw()方法内部的绘制过程总共可以分以下几步:
  1,对视图的背景进行绘制。这里会先得到一个mBGDrawable对象,然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable的draw()方法来完成背景的绘制工作。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值。
  2,是对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()方法,那么onDraw()方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。
  3,是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。
  4,是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是ListView或者ScrollView,为什么要绘制滚动条呢?其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。
  通过以上流程分析,相信大家已经知道,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。Canvas这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西。

3,Android视图状态及重绘流程分析。
  相信大家在平时使用View的时候都会发现它是有状态的,比如说有一个按钮,普通状态下是一种效果,但是当手指按下的时候就会变成另外一种效果,这样才会给人产生一种点击了按钮的感觉。接下来我们来一起探究一下它背后的实现原理应该是什么样子的吧?
 (1)、几种重要的视图状态
 A,enabled。
 表示当前视图是否可用。可以调用setEnable()方法来改变视图的可用状态,传入true表示可用,传入false表示不可用。它们之间最大的区别在于,不可用的视图是无法响应onTouch事件的。
 B,focused。
 表示当前视图是否获得到焦点。通常情况下有两种方法可以让视图获得焦点,即通过键盘的上下左右键切换视图,以及调用requestFocus()方法。而现在的android手机几乎都没有键盘了,因此基本上只可以使用requestFocus()这个办法来让视图获得焦点了。而requestFocus()方法也不能保证一定可以让视图获得焦点,它会有一个布尔值的返回值,如果返回true说明获得焦点成功,返回false说明获得焦点失败。一般只有视图在focusable和focusable in touch mode同时成立的情况下才能成功获取焦点,比如说EditText。
 C,window_focused。
 表示当前视图是否处于正在交互的窗口中,这个值由系统自动决定,应用程序不能进行改变。
 D,selected。
 表示当前视图是否处于选中状态。一个界面当中可以有多个视图处于选中状态,调用setSelected()方法能够改变视图的选中状态,传入true表示选中,传入false表示未选中。
E,pressed.
表示当前视图是否处于按下状态。可以调用setPressed()方法来对这一状态进行改变,传入true表示按下,传入false表示未按下。通常情况下这个状态都是由系统自动赋值的,但开发者也可以自己调用这个方法来进行改变。
(2)视图重绘。
  虽然视图会在Activity加载完成之后自动绘制到屏幕上,但是我们完全有理由在与Activity进行交互的时候要求动态更新视图,比如改变视图的状态、以及显示或隐藏某个控件等。那在这个时候,之前绘制出的视图其实就已经过期了,此时我们就应该对视图进行重绘。
  调用视图的setVisibility()、setEnabled()、setSelected()等方法时都会导致视图重绘,都是通过调用invalidate()方法来实现的,那么就让我们来看一看invalidate()方法的代码是什么样的吧。
  在这个方法中首先会调用skipInvalidate()方法来判断当前View是否需要重绘,判断的逻辑也比较简单,如果View是不可见的且没有执行任何动画,就认为不需要重绘了。之后会进行透明度的判断,并给View添加一些标记位,然后调用ViewParent的invalidateChild()方法,这里的ViewParent其实就是当前视图的父视图,因此会调用到ViewGroup的invalidateChild()方法中。在invalidateChild()方法中,有一个while循环,当ViewParent不为空时,会一直不断地获取当前布局的父布局,并调用它的invalidateChildInParent()方法,在ViewGroup的invalidateChildInParent()方法中主要是来计算需要重绘的矩形区域,当循环到最外层的根布局后,就会调用ViewRoot的invalidateChildInParent()方法了,这个方法里就是去调用了invalidateChild()方法,而invalidateChild()中又调用了scheduleTraversals()这个方法。这scheduleTraversals()中调用了sendEmptyMessage()方法,并传入了一个DO_TRAVERSAL参数,会在ViewRoot的handleMessage()方法中接收到。在handleMessage()方法中调用了performTraversals()方法,这就是我们在前面一篇文章中学到的视图绘制的入口。
  虽然经过了很多辗转的调用,但是可以确定的是,调用视图的invalidate()方法后确实会走到performTraversals()方法中,然后重新执行绘制流程。
  另外需要注意的是,invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()了。这个方法中的流程比invalidate()方法要简单一些,但中心思想是差不多的,这里也就不再详细进行分析了。
 
4,Android自定义View的实现方法。
  如果说要按类型来划分的话,自定义View的实现方式大概可以分为三种:自绘控件、组合控件、以及继承控件。那么下面我们就来依次学习一下,每种方式分别是如何自定义View的。
  (1)自绘控件。
  自绘控件的意思就是,这个View上所展现的内容全部都是我们自己绘制出来的。绘制的代码是写在onDraw()方法中。
  例如:
  

public class CounterView extends View implements OnClickListener {      private Paint mPaint;      private Rect mBounds;      private int mCount;      public CounterView(Context context, AttributeSet attrs) {          super(context, attrs);          mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);          mBounds = new Rect();          setOnClickListener(this);      }      @Override      protected void onDraw(Canvas canvas) {          super.onDraw(canvas);          mPaint.setColor(Color.BLUE);          canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);          mPaint.setColor(Color.YELLOW);          mPaint.setTextSize(30);          String text = String.valueOf(mCount);          mPaint.getTextBounds(text, 0, text.length(), mBounds);          float textWidth = mBounds.width();          float textHeight = mBounds.height();          canvas.drawText(text, getWidth() / 2 - textWidth / 2, getHeight() / 2                  + textHeight / 2, mPaint);      }      @Override      public void onClick(View v) {          mCount++;          invalidate();      }  }  

首先我们在CounterView的构造函数中初始化了一些数据,并给这个View的本身注册了点击事件,这样当CounterView被点击的时候,onClick()方法就会得到调用。而onClick()方法中的逻辑就更加简单了,只是对mCount这个计数器加1,然后调用invalidate()方法。通过 Android视图状态及重绘流程分析,带你一步步深入了解View(三) 这篇文章的学习我们都已经知道,调用invalidate()方法会导致视图进行重绘,因此onDraw()方法在稍后就将会得到调用。

  (2)组合控件
  组合控件的意思就是,我们并不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但我们可以将几个系统原生的控件组合到一起,这样创建出的控件就被称为组合控件。
  举个例子来说,标题栏就是个很常见的组合控件,很多界面的头部都会放置一个标题栏,标题栏上会有个返回按钮和标题,点击按钮后就可以返回到上一个界面。那么下面我们就来尝试去实现这样一个标题栏控件。
  

<?xml version="1.0" encoding="utf-8"?>  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="match_parent"      android:layout_height="50dp"      android:background="#ffcb05" >      <Button          android:id="@+id/button_left"          android:layout_width="60dp"          android:layout_height="40dp"          android:layout_centerVertical="true"          android:layout_marginLeft="5dp"          android:background="@drawable/back_button"          android:text="Back"          android:textColor="#fff" />      <TextView          android:id="@+id/title_text"          android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:layout_centerInParent="true"          android:text="This is Title"          android:textColor="#fff"          android:textSize="20sp" />  </RelativeLayout>  

接下来创建一个TitleView继承自FrameLayout,代码如下所示:

public class TitleView extends FrameLayout {      private Button leftButton;      private TextView titleText;      public TitleView(Context context, AttributeSet attrs) {          super(context, attrs);          LayoutInflater.from(context).inflate(R.layout.title, this);          titleText = (TextView) findViewById(R.id.title_text);          leftButton = (Button) findViewById(R.id.button_left);          leftButton.setOnClickListener(new OnClickListener() {              @Override              public void onClick(View v) {                  ((Activity) getContext()).finish();              }          });      }      public void setTitleText(String text) {          titleText.setText(text);      }      public void setLeftButtonText(String text) {          leftButton.setText(text);      }      public void setLeftButtonListener(OnClickListener l) {          leftButton.setOnClickListener(l);      }  }  

(3)继承控件。
  继承控件的意思就是,我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。这种自定义控件的特点就是不仅能够按照我们的需求加入相应的功能,还可以保留原生控件的所有功能。
  例如:在ListView上滑动就可以显示出一个删除按钮,点击按钮就会删除相应数据的功能。
  在MyListView的构造方法中创建了一个GestureDetector的实例用于监听手势,然后给MyListView注册了touch监听事件。然后在onTouch()方法中进行判断,如果删除按钮已经显示了,就将它移除掉,如果删除按钮没有显示,就使用GestureDetector来处理当前手势。
  当手指按下时,会调用OnGestureListener的onDown()方法,在这里通过pointToPosition()方法来判断出当前选中的是ListView的哪一行。当手指快速滑动时,会调用onFling()方法,在这里会去加载delete_button.xml这个布局,然后将删除按钮添加到当前选中的那一行item上。注意,我们还给删除按钮添加了一个点击事件,当点击了删除按钮时就会回调onDeleteListener的onDelete()方法,在回调方法中应该去处理具体的删除操作。