1.自定义View

一、自定义View

1.自定义View的类型

1.1.组合控件。

1.2.扩展系统View控件功能。例如TextView,继承它并扩展它的功能。

1.3.继承View。新建一个新的控件。

1.4.继承ViewGroup系统控件。继承LinearLayout等系统控件,并扩展它的功能。

1.5.继承ViewViewGroup。新建一个新的ViewGroup控件。

 

2、View绘制流程

View的绘制基本与measure()、layout()、draw()这三个函数完成,也就是说绘制分三步,measure->layout->draw

2.1、measure()。它的作用是测量View的宽高。相关方法有measure()、setMeasuredDimension()、onMeasure()

2.2、layout()。计算当前View以及子View的位置。相关方法有:layout()、onLayout()、setFrame()

2.3、draw()。视图的绘制工作。draw(),onDraw()

 

3、坐标系

Android坐标系:以手机屏幕为坐标系,把屏幕左上角作为原点,这个原点向右是X轴的正轴,向下是Y轴正轴,如下图所示:

 

 

 View坐标系:它是以View内部作为一个坐标系

由上图可算出View的 高度:

width = getRight() - getLeft();

height = getBottom() - getTop();

View的源码当中提供了getWidth()和getHeight()方法用来获取View的宽度,其内部方法和上文所示是相同的,我们可以直接调用获取View得宽高。

 

获取View自身的坐标

通过如下方法可以获取View到其父控件的距离:

2.1.getTop():获取View到其父布局顶边的距离。

2.2.getLeft():获取View到其父布局左边的距离。

2.3.getBottom():获取View到其父布局顶边的距离。

2.4.getRight():获取View到父布局左边的距离。

4、构造函数

无论是我们继承系统View还是直接继承View,都需要对构造函数进行重写,构造函数有多个,至少要重写其中一个才行。如我们新建TestView。

5、自定义属性

Android系统的控件以Android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。

Android自定义属性可分为以下几步:

(1).自定义一个View

(2).编写values/attrs.xml,其中编写styleable和item等标签元素

(3).在布局文件中View使用自定义的属性(注意namespace)

 

新建一个resource文件,名字为values/attrs.xml,其内容如下所示:

<?xml version="1.0" encoding="utf-8"?>
<resources>
//自定义控件View
<declare-styleable name="test">
<attr name="text" format="string"/>
<attr name="testAttr" format="integer"/>
</declare-styleable>
</resources>

自定义View类
public class MyTextView extends View {
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test);
String text = ta.getString(R.styleable.test_text);
int arr = ta.getInteger(R.styleable.test_testAttr,-1);
System.out.println(text+","+arr);
ta.recycle();
}
}

在布局文件的使用
<com.jatpack.camerax01.ui.widget.MyTextView
android:layout_width="100dp"
android:layout_height="100dp"
app:text="测试"
app:testAttr="520"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@+id/confirm_btn"
/>

6.属性值的类型format
(1)、reference:参考某一资源ID
(2)、color颜色值
(3)、string
(4)、integer
(5)、boolean
(6)、dimension尺寸值
(7)、float浮点型
(8)、fraction百分数
(9)、enum枚举值。如下图所示
<attr name="orientation">
<enum name="horizontal" value="0"/>
<enum name="vertical" value="1"/>
</attr>
(10)、flag位或运算
<attr name="gravity">
<flag name="top" value="0x01"/>
<flag name="bottom" value="0x02"/>
<flag name="left" value="0x04"/>
<flag name="right" value="0x08"/>
<flag name="center_vertical" value="0x16"/>
</attr>
(11)、混合类型:属性定义时可以指定多种类型值

二、View绘制流程
View的绘制基本有measure()、layout()、draw()这三个函数完成
2.1、Measure()
MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。

MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。
即MeasureSpec当中一共存在三种mode:UNSPECIFIED、EXACTLY、AT_MOST
对于View来说,MeasureSpec的mode和Size有如下意义
(1)、EXACTLY:精确模式,View需要一个精确值,这个值即为MeasureSpe当中的Size。对应match_parent
(2)、AT_MOST:最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值。对应wrap_content
(3)、UNSPECIFIED:无限制,View对尺寸没有任何限制,View设置为多大就应当为多大
2.2、Layout()
layout()过程,对应View来说用来计算View的位置参数,对应ViewGroup来说,除了要测量自身位置,还需要测量子View的位置。
layout()方法是整个Layout()流程的入口,看一下这部分源码,进入View.layout有以下核心代码

if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

//这里通过setFrame或setOpticalFrame方法确定View在父容器当中的位置
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

//调用onLayout方法。onLayout方法是一个空实现,不同的布局会有不同的实现
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);

if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}

mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}

2.3、Draw()
draw流程也就是View的绘制,整个流程的入口在View的draw()方法中,整个过程分为7个步骤,源码的注释如下
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
* 7. If necessary, draw the default focus highlight
*/
(1)绘制背景
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();

(2)如有必要,保存当前画布,以便后面的绘制

(3)绘制内容
(4)绘制子View
(5)如有必要,绘制边缘等其他图层
(6)绘制装饰(例如滚动条)
(7)如有必要,绘制默认重点
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
onDraw(canvas);

// Step 4, draw the children
dispatchDraw(canvas);

drawAutofilledHighlight(canvas);

// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}

// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);

// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
。。。。。

3、自定义组合控件
自定义组合控件就是将多个控件组合成为一个新的控件,主要解决多次重复使用同一类型的布局。如我们顶部的HeaderView以及dailog等,我们都可以
把他们组合成一个新的控件。

我们通过一个自定义HeaderView实例来了解自定义组合控件的用法。
其全部代码如下
<declare-styleable name="HeaderBar">
<attr name="title_text_color" format="color"/>
<attr name="title_text" format="string"/>
<attr name="show_views">
<flag name="left_text" value="0x01"/>
<flag name="left_img" value="0x02"/>
<flag name="right_text" value="0x04"/>
<flag name="right_img" value="0x08"/>
<flag name="center_text" value="0x10"/>
<flag name="center_ing" value="0x20"/>
</attr>
</declare-styleable>
<com.jatpack.camerax01.ui.widget.YFHeaderView
android:id="@+id/nav_top"
android:layout_width="match_parent"
android:layout_height="48dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:title_text="拍照"
app:show_views="center_text|left_img|right_img"/>

<?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="48dp"
android:id="@+id/header_root_layout"
android:background="#827192">

<ImageView
android:id="@+id/header_left_img"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentLeft="true"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:scaleType="fitCenter"
android:src="@drawable/back"/>

<TextView
android:id="@+id/header_center_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:lines="1"
android:maxLines="11"
android:ellipsize="end"
android:text="title"
android:textStyle="bold"
android:textColor="#ffffff"/>

<ImageView
android:id="@+id/header_right_img"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="12dp"
android:layout_alignParentRight="true"
android:src="@drawable/add"
android:scaleType="fitCenter"/>

</RelativeLayout>
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.jatpack.camerax01.R;

public class YFHeaderView extends RelativeLayout {
public YFHeaderView(Context context) {
super(context);
initView(context);
}

public YFHeaderView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
initAttrs(context,attrs);
}

public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
initAttrs(context,attrs);
}

public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initView(context);
initAttrs(context,attrs);
}

RelativeLayout layoutRoot;
ImageView imgLeft;
ImageView imgRight;
TextView textCenter;

/**
* 初始化UI,可根据业务需求设置默认值
* @param context
*/
private void initView(Context context){
LayoutInflater.from(context).inflate(R.layout.headerview,this,true);
imgLeft = findViewById(R.id.header_left_img);
imgRight = findViewById(R.id.header_right_img);
textCenter = findViewById(R.id.header_center_text);
layoutRoot = findViewById(R.id.header_root_layout);
//layoutRoot.setBackgroundColor(Color.BLACK);
textCenter.setTextColor(Color.WHITE);
}

private void initAttrs(Context context,AttributeSet attrs){
TypedArray mTypedArray = context.obtainStyledAttributes(attrs,R.styleable.HeaderBar);
String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);
if(!TextUtils.isEmpty(title)){
textCenter.setText(title);
}
int showView = mTypedArray.getInt(R.styleable.HeaderBar_show_views,0x26);
int color = mTypedArray.getColor(R.styleable.HeaderBar_title_text_color, 100);
if(color != 100) {
textCenter.setTextColor(color);
}
mTypedArray.recycle();
showView(showView);
}

private void showView(int showView) {
//将showView转换为二进制数,根据不同位置上的值设置对应View的显示或者隐藏。
Long data = Long.valueOf(Integer.toBinaryString(showView));
String element = String.format("%06d", data);
for (int i = 0; i < element.length(); i++) {
if(i == 0) ;
if(i == 1) textCenter.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
if(i == 2) imgRight.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
if(i == 3) ;
if(i == 4) imgLeft.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
if(i == 5) ;
}

}

public void setTitle(String title){
if(title!=null&&title.trim().length()>0){
textCenter.setText(title);
}
}

public void setLeftListener(OnClickListener onClickListener){
imgLeft.setOnClickListener(onClickListener);
}

public void setRightListener(OnClickListener onClickListener){
imgRight.setOnClickListener(onClickListener);
}

}


4.继承系统控件
继承系统的控件可以分为继承View子类和继承ViewGroup子类。下面介绍继承View的方式。

需求:为字体设置背景,并在布局中间添加一条横线。

实现这种方式不需要全部重写系统的逻辑,上面我面说过绘制控件分三步onMeaseur、onlayout、onDraw,而通常
我们只需复写onDraw方法就可以了,其他两个方法完全可以复用不需要重写。其代码如下所示:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class LineTextView extends androidx.appcompat.widget.AppCompatTextView {
public LineTextView(@NonNull Context context) {
super(context);
init();
}

public LineTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

public LineTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

//定义画笔,用来绘制中心曲线
private Paint paint;

private void init(){
paint = new Paint();
paint.setColor(Color.BLACK);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

int width = getWidth();
int height = getHeight();

//设置画笔颜色为蓝色
paint.setColor(Color.BLUE);

//绘制蓝色方形背景
RectF rectF = new RectF(0,0,width,height);
canvas.drawRect(rectF,paint);

//绘制中心曲线,起点坐标(0,height/2),终点坐标(width,height/2)
//绘制画笔为黑色
paint.setColor(Color.BLACK);
//绘制中心曲线,起点坐标(0,height/2),终点坐标(width,height/2)
canvas.drawLine(0,height/2,width,height,paint);
}
}

5.直接继承View
上一种方式是继承系统控件,也就是已经修好的继承View的控件。直接继承View(与系统控件地位一样,可以看成同一类型)。
所以复用的方法会不一样。在这里需要重写onMeasure和onDraw方法。

根据View的源码中AT_MOST(最大限制模式)和ExACTLY(精确模式)并没有做出区分,两种的处理逻辑是一样的。也就是说wrap_content
和match_parent处理逻辑是一样的,都会是match_parent,显然与我们想要用的不一样,所以需要重写onMeasure方法。
重写onMeasure方法

直接继承View需要注意以下几点:
1、在onDraw当中对padding属性进行处理。
2、在onMeasure过程中对wrap_content属性进行处理。
3、至少要有一个构造方法。

public class RectView extends View {
public RectView(Context context) {
super(context);
init();
}

public RectView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}

//定义画笔
private Paint paint = new Paint();

private void init(){
paint.setColor(Color.BLUE);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取各个边距的padding值
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//获取绘制的View的宽度
int width = getWidth() - paddingLeft - paddingRight;
//获取绘制的View的高度
int height = getHeight() - paddingTop - paddingBottom;
//绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)
canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,paint);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(widthMeasureSpec);

//处理wrap_contented情况
if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(300,3000);
} else if(widthMode == MeasureSpec.AT_MOST){
setMeasuredDimension(300,heightMeasureSpec);
} else if(heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize,300);
}
}
}

6.继承ViewGroup
自定义的ViewGroup比自定义的View复杂一些,因为他还需要对子View的参数进行处理。下面我们将做一个例子
例子:实现一个类似于Viewpager的可左右滑动的布局
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

public class HorizontaiView extends ViewGroup {

private int lastX;
private int lastY;

private int currentIndex = 0;
private int childWidth = 0;
private Scroller scroller;
private VelocityTracker tracker;

public HorizontaiView(Context context) {
super(context);
init(context);
}

public HorizontaiView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}

public HorizontaiView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

public HorizontaiView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}

private void init(Context context){
scroller = new Scroller(context);
tracker = VelocityTracker.obtain();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取宽高的测量模式以及测量值
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//测量所有子View
measureChildren(widthMeasureSpec,heightMeasureSpec);
//如果没有子View,则View0,0
if(getChildCount() == 0){
setMeasuredDimension(0,0);
} else if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
View childOne = getChildAt(0);
int childWidth = childOne.getMeasuredWidth();
int childHeight = childOne.getMeasuredHeight();
//View的宽度=单个子View宽度*子View数,View的高度=子View的高度
setMeasuredDimension(getChildCount()*childWidth,childHeight);
} else if(widthMode == MeasureSpec.AT_MOST){
View childOne = getChildAt(0);
int childWidth = childOne.getMeasuredWidth();
//View的宽度=单个子View宽度*子View个数,View的高度=自己设置的高度
setMeasuredDimension(getChildCount()*childWidth,heightSize);
} else if(heightMode == MeasureSpec.AT_MOST){
View childOne = getChildAt(0);
int childHeight = childOne.getMeasuredHeight();
//View的宽度=xml当中设置的宽度,View的高度=子View高度
setMeasuredDimension(widthSize,childHeight);
}
}

/**
* 接下来重写onLayout方法,对各个子View设置位置
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int left = 0;
View child;
for(int i = 0;i < childCount;i++){
child = getChildAt(i);
if(child.getVisibility() != View.GONE){
childWidth = child.getMeasuredWidth();
child.layout(left,0,left + childWidth,child.getMeasuredHeight());
left += childWidth;
}
}
}

/**
* 因为我们定义的是ViewGroup,从onInterceptTouchEvent开始
* 重写onInterceptTouchEvent,对横向滑动事件进行拦截
* @param event
* @return
*/
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
//return super.onInterceptHoverEvent(event);
boolean intercrpt = false;
//记录当前点击的坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
int deltaX = x - lastX;
int deltaY = y - lastY;
//当X轴移动的绝对值大于Y轴移动的绝对值时,表示用户进行了横向滑动,对事件进行拦截
if(Math.abs(deltaX) > Math.abs(deltaY)){
intercrpt = true;
}
break;
}
lastX = x;
lastY = y;
//intercrpt = true表示对事件进行拦截
return intercrpt;
}

/**
* 当ViewGroup拦截下用户的横向滑动事件后,后续的Touch事件将交付给
* onTouchEvent进行处理,重写onTouchEvent方法
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
//return super.onTouchEvent(event);
tracker.addMovement(event);
//获取事件坐标(x,y)
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int deltaX = x - lastX;
int delatY = y - lastY;
//scrollBy方法将对我们当前View的位置进行偏移
scrollBy(-deltaX, 0);
break;
//当产生ACTION_UP事件时,也就是我们抬起手指
case MotionEvent.ACTION_UP:
//getScrollX()为在X轴方向发生的便宜,childWidth * currentIndex表示当前View在滑动开始之前的X坐标
//distance存储的就是此次滑动的距离
int distance = getScrollX() - childWidth * currentIndex;
//当本次滑动距离>View宽度的1/2时,切换View
if (Math.abs(distance) > childWidth / 2) {
if (distance > 0) {
currentIndex++;
} else {
currentIndex--;
}
} else {
//获取X轴加速度,units为单位,默认为像素,这里为每秒1000个像素点
tracker.computeCurrentVelocity(1000);
float xV = tracker.getXVelocity();
//当X轴加速度>50时,也就是产生了快速滑动,也会切换View
if (Math.abs(xV) > 50) {
if (xV < 0) {
currentIndex++;
} else {
currentIndex--;
}
}
}
//对currentIndex做出限制其范围为【0,getChildCount() - 1】
currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ? getChildCount() - 1 : currentIndex;
//滑动到下一个View
smoothScrollTo(currentIndex * childWidth, 0);
tracker.clear();
break;
}
lastX = x;
lastY = y;
return true;
}

private void smoothScrollTo(int destX, int destY) {
//startScroll方法将产生一系列偏移量,从(getScrollX(), getScrollY()),destX - getScrollX()和destY - getScrollY()为移动的距离
scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000);
//invalidate方法会重绘View,也就是调用View的onDraw方法,而onDraw又会调用computeScroll()方法
invalidate();
}

//重写computeScroll方法
@Override
public void computeScroll() {
super.computeScroll();
//当scroller.computeScrollOffset()=true时表示滑动没有结束
if (scroller.computeScrollOffset()) {
//调用scrollTo方法进行滑动,滑动到scroller当中计算到的滑动位置
scrollTo(scroller.getCurrX(), scroller.getCurrY());
//没有滑动结束,继续刷新View
postInvalidate();
}
}
}


posted @ 2021-04-12 18:20  周千  阅读(398)  评论(0编辑  收藏  举报