关于 Android 自定义控件,你想谈一些什么?
最近在开始深入的去学习Android自定义控件这块的知识,发现涉及到各方各面的知识点略多,如:
- View、ViewGroup的绘制
- 事件分发
- 各种动画效果
- 滚动嵌套机制
- 还有涉及到相关的数学知识等等
作为刚刚想深入学习自定义控件这块知识的孩纸,想知道那些擅长于写各种控件的大牛们,是怎样去一步一步学习到最终可以随心所欲的造控件的!坐等老司机开车!
Android开发自定义控件这个需求其实还是蛮常见的,Android标准控件库根本满足不了日益脑洞的产品和设计师.
自定义控件原则:一个好的自定义控件应当和Android本身提供的控件一样,封装了一系列的功能以供开发者使用,不仅具有完备的功能,也需要高效的使用内存和CPU。
开发自定义控件的步骤:
1、了解View的工作原理 ;
2、 编写继承自View的子类;
3、 为自定义View类增加属性 ;
4、 绘制控件 ;
5、 响应用户消息 ;
6 、自定义回调函数 。
Android本身提供了一些指标:
1. 应当遵守Android标准的规范(命名,可配置,事件处理等);
2. 在XML布局中科配置控件的属性;
3. 对交互应当有合适的反馈,比如按下,点击等;
4. 具有兼容性, Android版本很多,应该具有广泛的适用性。
接下来就看设计师和产品经理的脑洞啦
#1楼 @xiaochenyi get到不少点子哦,xiaochenyi童鞋在自定义控件这块已经颇具心得。我还是这一块的入门小白,目前进阶学习中,说说自己当前的学习模式吧:
1、看书,买了徐宜生的《Android群英传》正在啃;
2、看API文档和一些技术博客;(也即是你说的,了解View的工作原理)
3、挑一个系统控件代码来解读学习,目前的感受是,View和动画的知识这块随着Android版本升级也在不断更新变化;(所以需要考虑你提到的兼容性问题)
4、挑一个有开源代码的控件,来进行高仿实现,一步步深入掌握一些知识点;
5、直接看一些动态效果图,按照自己学习到的原理和思路进行实现;(可惜,目前我还没达到这一步,哭。。。。)
再说说自己学习过程中碰到的一些难点:
1、调试效果非常耗时,十天半个月真是见怪不怪;(有没有关于调试自定义控件这块的经验可以分享一下呢?)
2、对数学知识有一点要求,从动画效果这块可以看出,所以也在补习大学的功课中;
3、耐心,调试效果这块真的要沉得住气来慢慢调试;(反省一下自己有点心急,有待提高啊!)
写了这么多,补上感慨最深的一点:学习要持之以恒。
哈哈,xiaochenyi童鞋在学习自定义控件这块上,有木有更具体的经历可以再分享一下呢?
一、什么是自定义控件
1、概念
简单算来学习Android已经有一年时间了,从最初觉得别人写的软件好厉害到这么厉害的软件我也能写。但是现在还是会被有些软件的UI和动画所惊艳。一开始以为UI上的控件都是画出来的,后来才知道这些控件都有一个共同的名字——自定义控件。
为什么要自定义控件呢?当然不是为了简单的好看。我们知道Android官方自带的控件种类很多,基本能够满足日常的开发需求。但是一件产品的开发不仅仅需要功能上的完善,更要追求用户体验。所以单从用户体验上来说,官方的控件是远远谈不上体验的。所以越来越多的APP使用了自定义控件,一方面美观好看,另一方面极大的提高了用户体验,何乐而不为呢?
而随着Android技术越来越成熟,基本的控件有时已经满足不了简单的开发需求了,这个时候就需要我们自定义出满足功能需求的控件来实现APP的一些需求。
2、实现方式
一般实现自定义控件会有三种方式:
- 继承已有的控件实现
- 组合已有的控件实现
- 完全自定义控件
第一种方式其实也就相当于扩展已有控件的功能,这种实现方式比较简单;第二种组合方式目的是通过多种控件的组合来完成一种控件的需求,也就是通过这种方式自定义出来的控件具有多种基本控件的功能,更加强大,较第一种而言这种实现方式比较复杂;而第三种完全自定义控件这就更加复杂了,这需要我们新建一种控件继承View/ViewGroup,并实现一些其中的属性或方法。
总的来说,按照需求我们采取不同的方式。这里我们先说一下完全自定义控件的方式。
二、完全自定义控件
下面我就分享我最近学习黑马教程中的一个自定义开关的过程。
1、确定需求
从图中我们可以看出,这个开关是由两部分组成,第一部分是背景图也就是显示“开/关”的图片,第二部分是前景图也就是开关小滑块。那么第一步我们肯定是需要将两者组合在一起,成为一个一个全新的控件。所以说这一步中,我们需要绘制出控件的基本形状。
因为这个开关是一个滑动开关,需要用户手动触摸才能改变状态,那么我们肯定需要实现这个控件的触摸事件,通过触摸事件来改变开关的状态。
开关的状态既然需要改变,那么如何知道状态发生改变呢?没错,就是事件监听。我们还需要将这个控件绑定事件监听器,来实时监听开关状态的改变。
一个控件有了形状,有了触摸事件和状态监听器,就已经能实现一些基本的功能需求了。所以总的来说,我们需要做三件事情:
- 绘制控件
- 触摸事件监听
- 状态事件监听
下面我就按照顺序来实现相关的功能。
2、绘制控件
首先我们新建一个 CustomSwitchView 类,类直接继承于View。继承类过后我们需要实现类的几种构造方法。在这里如果用Eclipse新建类的话,我们可以直接勾选 Constructors from superclass 选项。用Android Studio新建类的话,我们可以在类建立过后利用快捷键 Alt + Enter 来实现构造方法。
/**
* @ClassName: CustomSwitchView
* @Description:自定义控件 继承View
* @author: iamxiarui@foxmail.com
* @date: 2016年5月5日 下午6:51:49
*/
public class CustomSwitchView extends View {
/**
* @Description:用于代码创建控件
*/
public CustomSwitchView(Context context) {
super(context);
}
/**
* @Description:用于在XML中使用,可以指定自定义属性
*/
public CustomSwitchView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* @Description:用于在XML中使用,可以指定自定义属性,并指定样式
*/
public CustomSwitchView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* @Description:用于在XML中使用,可以指定自定义属性,并指定样式及其资源
*/
public CustomSwitchView(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
当构造函数实现之后,我们就需要实现控件的一些属性。这里我们先不用自定义属性,而用自定义的方法来设置相关属性。先定义如下变量,后面我们需要用到:
//定义背景图
private Bitmap switchBackgroupBitmap;
//定义前景图
private Bitmap switchForegroupBitmap;
变量定义好之后我们需要自定义两个方法,分别设置前景图和背景图,而两个方法的参数都是一个 int 类型的资源ID,然后通过BitmapFactory对象来将资源ID对应的图片资源添加到控件上:
/**
* @Title: setBackgroundPic
* @Description:设置背景图
* @return: void
*/
public void setBackgroundPic(int switchBackground) {
switchBackgroupBitmap = BitmapFactory.decodeResource(getResources(), switchBackground);
}
/**
* @Title: setForegroundPic
* @Description:设置前景图
* @return: void
*/
public void setForegroundPic(int switchForeground) {
switchForegroupBitmap = BitmapFactory.decodeResource(getResources(), switchForeground);
}
注意这个时候不是说我们设置上图片就能显示出来,因为我们是自定义控件,所以我们必须将控件绘制在View中,这就涉及到一个非常重要知识——Android界面绘制流程。
从图中我们可以看出Android界面绘制流程分为三个部分,第一部分是测量(Measure),在这部分里面View会先做一次测量,计算出自己需要占用多大的面积,我们可以重写 onMeasure() 方法来重新定义View的宽高。第二部分是布局(Layout),这个部分我们需要做的事情就是将整个View中所有的子View大小宽高设置好,可以通过复写 onLayout() 方法来实现,当然如果你的自定义View中没有子View,那就不需要设计这一部分了。第三部分是绘制(Draw),这个很好理解,就是在创建的画布(Canvas)上绘制出我们所需要的View样式,同样可以通过复写 onDraw() 方法来实现。
由于我们现在所要做的就是一个简单开关,只需要直接继承View,并将开关的两张图设置成控件的背景即可,所以我们只要重写 onMeasure() 和 onDraw() 这两个方法。
/**
* @Title: onMeasure
* @Description:测量出自定义控件的长宽
* @return: void
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(switchBackgroupBitmap.getWidth(), switchBackgroupBitmap.getHeight());
}
/**
* @Title: onDraw
* @Description:绘制控件
* @return: void
*/
@Override
protected void onDraw(Canvas canvas) {
// 先绘制背景
canvas.drawBitmap(switchBackgroupBitmap, 0, 0, paint);
//再绘制前景
canvas.drawBitmap(switchForegroupBitmap, 0, 0, paint);
}
当上面的方法重写完毕后,我们就可以在Activity中设置图片并显示控件了:
buttonCSView = (CustomSwitchView) findViewById(R.id.csv_button);
// 设置背景图
buttonCSView.setBackgroundPic(R.drawable.switch_background);
// 设置前景图
buttonCSView.setForegroundPic(R.drawable.switch_foreground);
当然在此之前,我们需要一个画笔工具,因为每一个画笔都需要在创建的时候使用,所以我将画笔工具的创建放在单独的方法中,而且在每一个构造函数中,调用这个方法,也就相当于只要创建了自定义控件,那么就自动创建了一个画笔工具。
/**
* @Title: initView
* @Description:初始化View
* @return: void
*/
private void initView() {
paint = new Paint();
}
除此之外呢,还需要在布局文件中定义出控件,注意一定要写View所在类的完整包名,在这里我的包名是xr.customswitch.view.CustomSwitchView
<xr.customswitch.view.CustomSwitchView
android:id="@+id/csv_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />;
好了进行到这一步的话,我们的自定义控件就算绘制出来了。但是一个控件绘制出来还不算一个完整的控件,所以我们还需要添加一些事件监听。
3、触摸事件
在写触摸事件之前,我们需要声明一些参数。首先开关在开或者关的时候一定有个状态(isSwitchState),我们必须要根据这个状态来处理一些逻辑问题,所以这个状态我们必须要明确。其次由于是触摸事件,所以我们还需要一个触摸状态(isTouchState),根据触摸状态我们处理触摸事件逻辑。而如何知道开关状态和触摸状态呢,当然是根据前景图中的开关滑块相对于背景图的位置来确定,而这个开关一定是处于背景图中的,不能超过背景图的范围,所以我们必须明确当前开关位置(currentPosition)和这个开关能滑动的最大位置(maxPosition)。
private boolean isSwitchState = true; //开关状态
private boolean isTouchState = false; //触摸状态
private float currentPosition; // 当前开关位置
private int maxPosition; // 开关滑动最大位置
定义好相关参数及变量后,我们需要知道开关位置的参数规定,直接上图吧。
下面我们就需要重写 onTouchEvent( )与 onDraw() 方法了,由于本文主要讲的是自定义控件的步骤,所以具体逻辑上的处理就看注释吧,写的很详细:
/**
* @Title: onTouchEvent
* @Description:触摸事件
* @return: void
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 处于触摸状态
isTouchState = true;
// 得到位置坐标
currentPosition = event.getX();
break;
case MotionEvent.ACTION_MOVE:
currentPosition = event.getX();
break;
case MotionEvent.ACTION_UP:
// 触摸状态结束
isTouchState = false;
currentPosition = event.getX();
// 中间标志位置
float centerPosition = switchBackgroupBitmap.getWidth() / 2.0f;
// 如果开关当前位置大于背景位置的一半 显示关 否则显示开
boolean currentState = currentPosition > centerPosition;
// 当前状态置为开关状态
isSwitchState = currentState;
break;
}
// 重新调用onDraw方法,不断重绘界面
invalidate();
return true;
}
/**
* @Title: onDraw
* @Description:绘制控件
* @return: void
*/
@Override
protected void onDraw(Canvas canvas) {
// 先绘制背景
canvas.drawBitmap(switchBackgroupBitmap, 0, 0, paint);
// 如果处于触摸状态
if (isTouchState) {
// 触摸位置在开关的中间位置
float movePosition = currentPosition - switchForegroupBitmap.getWidth() / 2.0f;
maxPosition = switchBackgroupBitmap.getWidth() - switchForegroupBitmap.getWidth();
// 限定开关滑动范围 只能在 0 - maxPosition范围内
if (movePosition < 0) {
movePosition = 0;
} else if (movePosition > maxPosition) {
movePosition = maxPosition;
}
// 绘制开关
canvas.drawBitmap(switchForegroupBitmap, movePosition, 0, paint);
}
// 直接绘制开关
else {
// 如果是真,直接将开关滑块置为开启状态
if (isSwitchState) {
maxPosition = switchBackgroupBitmap.getWidth() - switchForegroupBitmap.getWidth();
canvas.drawBitmap(switchForegroupBitmap, maxPosition, 0, paint);
} else {
// 否则将开关置为关闭状态
canvas.drawBitmap(switchForegroupBitmap, 0, 0, paint



