Android自定义控件

在实际项目的自定义View过程 

1,拿到需求,判断是否能通过拆分为 简单的View 实现、是否能够隔离事件分发等难题
最终达到的效果:
1)尽量不重写
2)单个View不涉及多的点击事件,不涉及复杂的绘制工作

2,通过步骤1,筛选到只需要绘制简单的View样式
系统的View执行流程为:构造方法 -> onMeasure -> onLayout -> onDraw
由于我们最终要的是效果,因此直接在onDraw中绘制想要的效果,再依次去调整构造方法、onMeasure、onLayout的数据

实际开发中自定义View如果按实现方式大概可以分为三种:自绘、组合、继承
1,自绘View:大部分是继承View,利用onDraw完成绘制,如:圆形进度条;
2,组合View:继承ViewGroup,简单场景直接使用xml已有布局,复杂场景onMeasure/onLayout/onDraw都可能用到,如:日历控件;
3,继承View:继承特定View,使用父控件的特性,并进行一定的扩展,如:自定义WebView;

在自定义View时,我们通常会去重写onDraw()方法来绘制View的显示内容。如果该View还需要使用wrap_content属性,那么还必须重写onMeasure()方法。另外,通过自定义attrs属性,还可以设置新的属性配置值。 在View中通常有以下一些比较重要的回调方法

onFinishInflate() 回调方法,当应用从XML加载该组件并用它构建界面之后调用的方法 
onMeasure() 检测View组件及其子组件的大小 
onLayout() 当该组件需要分配其子组件的位置、大小时 
onSizeChange() 当该组件的大小被改变时 
onDraw() 当组件将要绘 制它的内容时 
onKeyDown 当按下某个键盘时 
onKeyUp 当松开某个键盘时 
onTrackballEvent 当发生轨迹球事件时 
onTouchEvent 当发生触屏事件时 
onWindowFocusChanged(boolean) 当该组件得到、失去焦点时 
onAtrrachedToWindow() 当把该组件放入到某个窗口时 
onDetachedFromWindow() 当把该组件从某个窗口上分离时触发的方法 
onWindowVisibilityChanged(int): 当包含该组件的窗口的可见性发生改变时触发的方法 

 自定义控件更新方法

1、invalidate():
invalidate方法用于在UI线程中请求重绘视图。当我们希望在主线程中更新UI时,可以在UI线程中直接调用invalidate()方法。

内部实现:调用了invalidate方法后,为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,开始View树重绘流程(只绘制需要重绘的视图)

2、postInvalidate():
postInvalidate方法用于在非UI线程中请求重绘视图。如果我们希望在非UI线程中更新UI,就需要使用postInvalidate()方法。

内部实现:当调用postInvalidate()方法时,系统会通过Handler将重绘任务添加到主线程的消息队列中。在UI线程的消息循环中,当处理到该消息时,会触发invalidate()方法来进行实际的重绘操作。这样可以确保在非UI线程中请求的重绘操作在主线程中执行,避免多线程并发导致的问题。

3、requestLayout():
requestLayout方法用于请求重新布局视图。当我们希望更新视图的尺寸或位置时,需要调用requestLayout()方法。该方法会触发视图层次结构的重新测量、布局和绘制过程。例如,当修改了视图的宽度、高度、位置或其他布局参数时,应调用requestLayout()方法来请求重新布局。

调用invalidate方法只会执行onDraw方法;调用requestLayout方法只会执行onMeasure方法和onLayout方法,并不会执行onDraw方法。
所以当我们进行View更新时,若仅View的显示内容发生改变且新显示内容不影响View的大小、位置,则只需调用invalidate方法;若View宽高、位置发生改变且显示内容不变,只需调用requestLayout方法;若两者均发生改变,则需调用两者,按照View的绘制流程,推荐先调用requestLayout方法再调用invalidate方法。 

invalidate 和 postInvalidateinvalidate方法只能用于UI线程中,在非UI线程中,可直接使用postInvalidate方法,这样就省去使用handler的烦恼。 

一、对现有控件进行扩展

这是一个非常重要的自定义View方法,它可以在原生控件的基础上进行拓展,增加新的功能、修改显示的UI等。一般来说我们可以在onDraw()方法中对原生控件行为进行拓展。 以一个TextView为例,使用Canvas对象来进行图像的绘制,然后利用Android的绘图机制,可以绘制出更加复杂丰富的图像。比如可以利用LinearGradient Shader和Matrix来实现一个动态的文字闪动效果,要想实现这一个效果,可以充分利用Android中Paint对象的Shader渲染器。通过设置一个不断变化的LinearGradient,并使用带有该属性的Paint对象来绘制要显示的文字。首先,在onSizeChanged()方法中进行一些对象的初始化工作,并根据View的宽度设置一个LinearGradient渐变渲染器,代码如下:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh){
    super.onSizeChanged(w, h, oldw, oldh);
    if(mViewWidth  == 0) {
        mViewWidth = getMeasuredWidth();
        if(mViewWidth > 0) {
            mPaint = getPaint();
            // 创建mLinearGradient渐变渲染器并填充到画笔mPaint
            mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, 
                    new int[] {Color.BLUE, 0xffffffff, Color.BLUE}, 
                    null, Shader.TileMode.CLAMP);
            mPaint.setShader(mLinearGradient);
            mGradientMatrix = new Matrix();
        }
    }
}

 其中最关键的就是使用getPaint()方法获取当前绘制TextView的Paint对象,并给这个Paint对象设置原生TextView没有的LinearGradient属性。最后,在onDraw()方法中,通过矩阵的方式来不断平移渐变效果,从而在绘制文字时,产生动态的冷却效果,代码如下所示:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if(mGradientMatrix != null) {
        mTranslate += mViewWidth / 5;
        if(mTranslate > 2 * mViewWidth) {
            mTranslate = -mViewWidth;
        }
        mGradientMatrix.setTranslate(mTranslate, 0);  // 设置位移
        mLinearGradient.setLocalMatrix(mGradientMatrix);
        postInvalidateDelayed(100);  // 延时刷新
    }
}

这样就完成了对TextView进行拓展,制作一个动态文字闪动的TextView。 

二、创建复合控件

创建复合控件可以很好地创建出具体重用功能的控件集合。这种方式通常需要继承一个合适的ViewGroup,再给它添加指定功能的控件,从而组合成新的复合控件。通过这种方式创建的控件,我们一般会给它指定一些可配置的属性,让它具有更强的拓展性。 下面就以一个通用的TopBar为示例,进行讲解如何创建复合控件。

首先新建一个TopBar类继承RelativeLayout。

public class TopBar extends RelativeLayout {

    public TopBar(Context context) {
        this.TopBar(context, null);
    }

    public TopBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化的方法
        // 初始化属性
        initAttr(context, attrs)
        // 初始化布局
        initView(context);
        // 初如化事件
        initEvent();
    }

}
 1 定义属性: 

为一个View提供可自定义的属性非常简单,只需要在res资源目录的values目录下创建一个attrs.xml的属性定义文件,并在该文件中通过如下代码定义相应的属性即可。

<?xml version="1.0" encoding="utf-8"?>

<resources>

    <declare-styleable name="TopBar">
        <!-- 定义title文字,大小,颜色 -->
        <attr name="title" format="string" />
        <attr name="titleTextSize" format="dimension" />
        <attr name="titleTextColor" format="color" />
        <!-- 定义left 文字,大小,颜色,背景 -->
        <attr name="leftTextColor" format="color" />
        <attr name="leftTextSize" format="dimension" />
        <!-- 表示背景可以是颜色,也可以是引用 -->
        <attr name="leftBackground" format="reference|color" />
        <attr name="leftText" format="string" />
        <!-- 定义right 文字,大小,颜色,背景 -->
        <attr name="rightTextColor" format="color" />
        <attr name="rightTextSize" format="dimension"/>
        <attr name="rightBackground" format="reference|color" />
        <attr name="rightText" format="string" />
    </declare-styleable>

</resources>

我们在代码中通过标签声明了使用自定义属性,并通过name属性来确定引用的名称,通过标签来声明具体的自定义属性,比如在这里定义了标题文字的内容、大小、颜色,左右按钮的背景、文字内容、颜色等属性,并通过format属性来指定属性的类型。这里需要注意的是,有些属性可以是颜色属性,也可以是引用属性。比如按键的背景,所以使用“|”来分隔不同的属性——"reference|color"。 在确定好属性后,就可以创建一个自定义控件——TopBar,并让它继承自ViewGroup,从而组合一些需要的控件。这里为了简单,我们继承RelativeLayout。在构造方法中,通过如下所示代码来获取在XML布局文件中自定义的那些属性,即与我们使用系统提供的那些属性一样:

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);

系统提供了TypedArray这样的数据结构来获取自定义属性集,后面引用的styleable的TopBar,就是我们在XML中通过所指定的name名。接下来,通过TypeArray对象的getString()、getColor()等方法,就可以获取这些定义的属性值,代码如下所示:

private int mLeftTextColor;
private Drawable mLeftBackground;
private String mLeftText;
private float mLeftTextSize;

private int mRightTextColor;
private Drawable mRightBackground;
private String mRightTextSize;
private float mRightTextSize;

private String mTitleText;
private float mTitleTextSize;
private int mTitleTextColor;

private void initAttr(Context context, AttributeSet attrs) {
    // 通过这个方法,将你在attrs.xml中定义的declare-styleable的所有属性的值存储到TypedArray.
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
    // 从TypedArray中取出对应的值来为要设置的属性赋值
    mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
    mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
    mLeftText = ta.getString(R.styleable.TopBar_leftText);
    mLeftTextSize = typed.getDimension(R.styleable.TitleBar_leftTextSize, 20);

    mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
    mRightBackground = ta.getDrawable(R.styleable.TopBar_rightBackground);
    mRightText = ta.getString(R.styleable.TopBar_rightText);
    mRightTextSize = typed.getDimension(R.styleable.TitleBar_rightTextSize, 20);

    mTitleText = ta.getString(R.styleable.TopBar_titleText);
    mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
    mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);

// 获取完TypedArray的值后,一般要调用recyle()方法来避免重新创建的时候的错误
ta.recycle();
}

这里需要注意的是,当获取完所有的属性值后,需要调用TypedArray的recyle()方法来完成资源的回收。 

2 组合控件:

接下来,我们就可以开始组合控件了。TopBar由三个控件组成,左边按钮mLeftButton、右边按钮mRightButton、中间标题栏mTitleView。通过动态添加控件的方式,使用addView()方法将三个控件加入到定义的TopBar模板中,并给它们设置我们前面所获取到的具体的属性值,比如标题的文字、颜色、大小等,代码如下所示。

private TextView mTitleView;
private Button mLeftButton;
private Button mRightButton;

private RelativeLayout.LayoutParams mLeftParams;
private RelativeLayout.LayoutParams mRightParams;
private RelativeLayout.LayoutParams mTitleParams;

private void initView(Context context) {
    mTitleView = new TextView(context);
    mLeftButton = new Button(context);
    mRightButton = new Button(context);

    // 为创建的组件赋值,值就来源于引用的xml文件中给对应属性的赋值
    mTitleView.setText(mTitleText);
    mTitleView.setTextSize(mTitleTextSize);
    mTitleView.setTextColor(mTitleTextColor);
    mTitleView.setGravity(Gravity.CENTER);

    mLeftButton.setText(mLeftText);
    mLeftButton.setTextColor(mLeftTextColor);
    mLeftButton.setBackgroundDrawable(mLeftBackground);
    mLeftButton.setTextSize(mLeftTextSize);

    mRightButton.setText(mRightText);
    mRightButton.setTextSize(mRightTextSize);
    mRightButton.setBackgroundDrawable(mRightBackground);
    mRightButton.setTextColor(mRightTextColor);

    // 为组件元素设置相应的布局元素
    // 设置布局的layout_width和layout_height属性
    mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
    // 该方法表示所设置节点的属性必须关联其他兄弟节点或者属性值为布尔值。
    mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
    // 动态添加组件
    addView(mLeftButton, mLeftParams);

    mRightParams= new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
    mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
    addView(mRightButton, mRightParams);

    mTitleParams= new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
    mTitleParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
    addView(mTitleView, mTitleParams);
}

既然是UI模板,那么每个调用者所需要这些按钮的实现功能都是不一样的。因此,不能直接在UI模板里实现逻辑,可以通过接口回调的思想,实现逻辑交给调用者。实现过程如下所示。

  • 定义接口 定义一个左右按钮点击的接口,并创建两个方法,分别用于左右两个按钮的点击,代码如下所示。
// 在类内部定义一个接口对象,实现回调机制,不用去考虑如何实现,具体实现由调用者去创建
public interface OnClickListener{
    // 左按钮点击事件
    void leftClick();
    // 右按钮点击事件
    void rightClick();
    }
  • 暴露接口给调用者 在模板方法中,为左右按键增加点击事件,但不实现具体逻辑,而是调用接口中相应的点击方法,代码如下所示。

// 创建一个接口对象
private OnClickListener mListener;

// 暴露一个方法给调用者来注册接口,通过接口来获得回调者对接口方法的实现
public void setOnClickListener(OnClickListener listener) {
    this.mListener = listener;
}

private void initEvent(){
    // 按钮的点击事件,不需要具体的实现,只需要调用接口方法,回调的时候会有具体实现
    mLeftButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            mListener.leftClick();
        }
    });

    mRightButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            mListener.rightClick();
        }
    });
}
  • 实现接口回调 在调用者的代码中,调用者需要实现这样一个接口,并完成接口中的方法,确定具体的实现逻辑,并使用第二步中暴露的方法,将接口的对象传递进去,从而完成回调。通常情况下,可以使用匿名内部类的形式来实现接口中的方法,代码如下所示。
mTopBar.setOnClickListener(new TopBar.OnClickListener() {
    @Override
    public void leftClick() {
        // 点击左边按钮
    }

    @Override
    public void rightClick() {
        // 点击右边按钮
    }
}

在方法中分别添加点击左右按钮之后的逻辑代码。

除了通过接口回调的方式来实现动态的控制UI模板,同样可以使用公共方法来动态地修改UI模板中的UI,这样就进一步提高了模板的可定制性,代码如下所示:

public static final int LEFT = 1;
public static final int RIGHT = 2;

/**
  * 设置按钮的显示与否通过常量区分,visible区分是否显示
  *
  * @param view  标记View
  * @param visible  是否显示
  * /
public void setVisable(int view, int visible){
    switch(view) {
        case LEFT:
            mLeftButton.setVisibility(visible);
        break;
        case RIGHT:
            mRightButton.setVisibility(visible);
        break;
    }
}

通过如上代码,调用者通过TopBar对象调用这个方法后,根据参数,可以动态地控制按钮的显示,代码如下:

// 控制TopBar上组件的状态
mTopBar.setVisable(TopBar.LEFT, View.VISIBLE);
mTopBar.setVisable(TopBar.RIGHT, View.GONE);
3 引用UI模板:

最后一步,自然是在需要使用的地方引用UI模板,在引用前,需要指定引用第三方控件的名字空间:

xmlns:app="http://schemas.android.com/apk/res/res-auto"
<com.example.demo.TopBar
        android:id="@+id/tb"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_alignParentBottom="true"
        app:leftBackGround="#ff000000"
        app:leftText="Back"
        app:leftTextColor="#ffff6734"
        app:leftTextSize="25dp" 
        app:rightText="More"
        app:rightTextSize="25dp"
        app:rightTextColor="#ff123456"
        app:title="自定义标题"
        app:titleTextColor="#ff654321"/>

使用自定义的View与系统原生的View最大的区别就是在申明控件时,需要指定完整的包名,而在引用自定义的属性时,需要使用自定义的xmlns名字。

三、重写View来实现全新控件

当Android系统原生控件无法满足我们的需求的时候,我们就可以完全创建一个新的自定义View来实现需要的功能。创建一个自定义View,难点在于绘制控件和实现交互,这也是评价一个自定义View优劣的标准之一。 同时需要继承View类,并重写它的onDraw()、onMeasure()等方法来实现绘制逻辑,同时通过重写onTouchEvent()等触控事件来实现交互逻辑。也可以像实现组合控件方式,通过引入自定义属性,丰富自定义View的可定制性。

音频条形图

只演示自定义View的用法,不真实地监听音频输入,随机模拟一些数字即可。先看一下最终实现的效果图,如下图所示

如果要实现这样的效果,也就是绘制一个个的矩形,每个矩形之间稍微偏移一点距离即可。为了实现动态效果,只要在onDraw()方法中再去调用invalidate()方法通知View进行重绘就可以了。如果直接重绘会刷新太快影响效果,因此,可以使用postInvalidateDelayed(300)来进行延时重绘。代码如下所示。

this.invalidate();
this.postInvalidateDelayed(300);

并且给绘制的Paint对象可以增加一个LinearGradient渐变效果。代码如下所示:

private int mWidth;//控件的宽度
private int mRectWidth;// 矩形的宽度
private int mRectHeight;// 矩形的高度
private Paint mPaint;
private int mRectCount;// 矩形的个数
private int offset = 5;// 偏移
private double mRandom;
private LinearGradient lg;// 渐变

public ScaleMap(Context context) {
    this(context, null);
}

public ScaleMap(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    initPaint();  // 这些要在这里设置,因为渐变效果
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 设置宽高
    setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}

// 初始化画笔
private void initPaint() {
    mPaint = new Paint();
    mPaint.setColor(Color.GREEN);
    mPaint.setStyle(Paint.Style.FILL);
    mRectCount = 12;
}

//重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    for (int i = 0; i < mRectCount; i++) {
        mRandom = Math.random();
        float currentHeight = (int) (mRectHeight * mRandom);
        canvas.drawRect((float) (mWidth * 0.4 / 2 + mRectWidth * i + offset * i), currentHeight,
        (float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1) + offset * i), mRectHeight, mPaint);
    }
    postInvalidateDelayed(300);
}

//重写onSizeChanged方法,给画笔加上渐变
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mWidth = getWidth();
    mRectHeight = getHeight();
    mRectWidth = (int) (mWidth * 0.6 / mRectCount);
    lg = new LinearGradient(0, 0, mRectWidth, mRectHeight, Color.GREEN, Color.BLUE, TileMode.CLAMP);
    mPaint.setShader(lg);
}
自定义View一步一步来,从最基本的效果开始,慢慢增加功能,绘制更复杂的效果。

自定义ViewGroup

ViewGroup存在的目的就是为了对其子View进行管理,为其子View添加显示、响应的规则。因此,自定义ViewGroup通常重写onMeasure()方法来对子View进行测量,重写onLayout()方法来确定子View的位置,重写onTouchEvent()方法增加响应事件。 准备实现一个类似Android原生控件ScrollView的自定义ViewGroup,自定义ViewGroup实现ScrollView所具有的上下滑动功能,但是在滑动的过程中增加一个黏性的效果,即当一个子View向上滑动大于一定距离后,松开手指,它将自动向上滑动,显示下 个子View。同理,如果滑动小于一定的距离,松开手指,它将自动滑动到开始的位置。(类似阻尼回弹?)

首先让自定义ViewGroup能够实现类似ScrollView的功能。 在ViewGroup能够滚动之前,需要先放置好它的子View。使用遍历的方式来通知子View对自身进行测量,代码如下所示:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int count = getChildCount();  // 返回子View的数量
    for (int i = 0; i < count; ++i) {
        View childView = getChildAt(i);  // 获取子View
        measureChild(childView, widthMeasureSpec, heightMeasureSpec);  // 调用子View的测量方法
    }
}

接下来,就要对子View进行放置位置的设定。让每个子View都显示完整的一屏,这样在滑动的时候,可以比较好地实现后面的效果。在放置子View前,需要确定整个ViewGroup的高度。在本例中,由于让每个子View占一屏的高度,因此整个ViewGroup的高度即子View的个数乘以屏幕的高度,我们通过如下代码来确定整个ViewGroup的高度。

// 设置ViewGroup的高度
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
mlp.height = mScreenHeight * childCount;
setLayoutParams(mlp);

在获取了整个ViewGroup的高度之后,就可以通过遍历来设定每个子View需要放置的位置了,直接通过调用子View的layout()方法,并将具体的位置作为参数传递进去即可,代码如下所示:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childCount = getChildCount();
    // 设置ViewGroup的高度
    MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
    mlp.height = mScreenHeight * childCount;
    setLayoutParams(mlp);
    for (int i = 0; i < childCount; ++i) {
        View child = getChildAt(i);
        if (child.getVisibility() != View.GONE) {
            child.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight);
        }
    }
}

在代码中主要是去修改每个子View的top和bottom这两个属性,让它们能依次排列下来。 通过上面的步骤,就可以将子View放置到ViewGroup中了。但此时的ViewGroup还不能响应任何触控事件,自然也不能滑动,因此我们需要重写onTouchEvent()方法,为ViewGroup添加响应事件。在ViewGroup中添加滑动事件,通常可以使用scrollBy()方法来辅助滑动。在onTouchEvent()的ACTION_MOVE事件中,只要使用scrollBy(0,dy)方法,让手指滑动的时候让ViewGroup中的所有子View也跟着滚动dy即可,计算dy的方法有很多,如下代码就提供了一种思路。

case MotionEvent.ACTION_DOWN:
    mLastY = y;
    break;
case MotionEvent.ACTION_MOVE:
    if (!mScroller.isFinished()) {
        mScroller.abortAnimation();
    }
    int dy = mLastY - y;
    if (getScrollY() < 0 || getScrollY() > getHeight() - mScreenHeight) {
        dy = 0;
    }
    scrollBy(0, dy);
    mLastY = y;
    break;

按如上方法操作就可以实现类似ScrollView的滚动效果了。当然,系统的原生ScrollView有更大的功能,比如滑动的惯性效果等,这些功能可以在后面慢慢添加,这也是一个控件的迭代过程。 最后,我们来实现这个自定义ViewGroup的黏性效果。要实现手指离开后ViewGroup的黏性效果,我们很自然地想到onTouchEvent()的ACTION_UP事件和Scroller类。在ACTION_UP事件中判断手指滑动的距离,如果超过一定距离,则使用Scroller类来平滑移动到下一个子View;如果小于一定距离,则回滚到原来的位置,代码如下所示:

@Override
public boolean onTouchEvent(MotionEvent event) {
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 记录触摸起点
            mStart = getScrollY();
            mLastY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }
            int dy = mLastY - y;
            if (getScrollY() < 0 || getScrollY() > getHeight() - mScreenHeight) {
                dy = 0;
            }
            scrollBy(0, dy);
            mLastY = y;
            break;
        case MotionEvent.ACTION_UP:
            // 记录触摸终点
            mEnd = getScrollY();
            int dScrollY = mEnd - mStart;
            if (dScrollY > 0) {
                if (dScrollY < mScreenHeight / 3) {
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, -dScrollY);
                } else {
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, mScreenHeight - dScrollY);
                }
            } else {
                if (-dScrollY < mScreenHeight / 3) {
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, -dScrollY);
                } else {
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, -mScreenHeight - dScrollY);
                }
            }
            break;
    }
    postInvalidate();
    return true;
}

当然,最后不要忘记加上computeScroll()的代码,如下所示:

// 由父视图调用,用来请求子视图根据偏移值 mScrollX,mScrollY 重新绘制
@Override
public void computeScroll() {
    super.computeScroll();
    if(mScroller.computeScrollOffset()){
        scrollTo(0,mScroller.getCurrY());
        postInvalidate();
    }
}

为了易于控制滑屏控制,Android框架提供了 computeScroll()方法去控制这个流程。在绘制View时,会在draw()过程调用该方法。因此, 再配合使用Scroller实例,我们就可以获得当前应该的偏移坐标,手动使View/ViewGroup偏移至该处。

总结

最后,让我们来总览一下自定义View / ViewGroup时调用的各种函数的顺序,如下图所示:

 参考:
Android invalidate、postInvalidate、requestLayout的区别

Android自定义控件

创建自定义视图组件  |  Views  |  Android Developers

  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值