第四届字节跳动青训营讲师非常用心给大家整理了课前、中、后的学习内容,同学们自我评估,选择性查漏补缺,便于大家更好的跟上讲师们的节奏,祝大家学习愉快,多多提问交流~
第一节:Android 系统及客户端概览
1、开发和交付
在移动互联网的世界中,系统是怎么运行的?最简单的形式就是下图:
- 人们了解世界的方式,在过去100多年发生了翻天覆地的变化,目前在移动互联的世界,主要手段是手机和app的媒介
- 提供信息的是app,表面是人和机器的关系,本质上还是人与人的关系。产品经理根据人的需求和世界的信息,形成需求,研发工程师来实现,再呈现给用户。所以背后有着一大批人在负责创造和维护这个沟通媒介。
-
深入到更详细的内部流程,客户端开发只是直接面向用户的人,背后有更多的支撑团队,包括服务端团队提供网络数据,而数据可以来自推荐团队的处理。此外还经过QA同学的测试,保证质量的情况下,发布给用户。
- 如果对目前市场上的应用做一个分类排序,我们可以整理成下面的图。每个分类的宽度代表用户的使用时长,然后每个分类里面按照市占率最高往下排序。可以看到绝大多数的互联网大厂都在这个表格里面。表格里面的位置不是精确值,更多是一种估算。
\
2、Android 知识图谱
客户端是典型的应用型岗位,之前的知识图谱可能都是基于知识线的梳理,这里我们从交互的整个系统中着眼,看为了满足上面的交付过程,作为客户端开发我们应该掌握什么样的知识体系。梳理完成之后,你就会发现:几乎所有技能点都能在客户端找到自己的试验场。
\
我们从宏观上,再简化上面的图,对于一个客户端开发来讲,我们面对的角色主要包括:
- 对外(用户):为用户创造价值,是最终的目的和宗旨,也是整个系统存在的前提
- 对内(公司):上面第二个图对应的是更详细的内部交付涉及的团队,实际上比这要复杂的多。这些团队组成了公司这个实体。对内都是成本,对外才是收益。对于内部来讲,我们的目的是降低交易成本。
-
自身(个人):我们通过自身的努力来推进交付流程,提供给用户好用的产品。同时个人也是一个产品,我们需要打造自身的技术品牌,培养自己的技术实力。
所以,从交付上看,包括对外、对内和自身三种不同的交付,每种交付都包括更多的层次,不同的层级对应了不同的知识要求。
\
对外 - 为用户创造价值
1、第一层交付:页面+逻辑+数据
这次层交互是最基本的,需要给用户展示交互良好的页面,提供符合预期的逻辑功能,并且获取和展示数据。用户大部分就可以得到满足。
技能点:复杂的交互,清晰的逻辑,网络基础
实际中的例子:
2、第二层交付:多样性需求
当用户最简单的了解信息的需求满足之后,我们需要进一步满足更多样化的诉求,比如多媒体内容,直播流观看,甚至游戏、AR等。这些需求背后需要更多的知识来支撑。
技能点:多媒体基础,OpenGL,音视频编解码,游戏开发
3、第三层交付:体验+质量+安全+个性化
当用户多样化的需求被满足,我们可能提供的就是一个规模庞大的App,随着用户规模的增加,用户诉求也就更高。特别是在目前的移动互联网时代,体验、质量和安全等方面的需求急速扩大。
我们在进入更精细化的需求,对用户需求的满足解决了覆盖面的问题,接下来就是匹配度的问题了。每个人都希望自己体验的是个性化的功能和内容,这大部分是个性化推荐团队来承接的,但随着用户对时效的要求越来越高,我们就需要更进一步的靠近用户。
技能点:Android系统,底层引擎,安全,Hook,机器学习,端智能
\
对内 - 减少公司成本
1、第一层交付:单人效能
效能提升的第一层是单人效能,这里的单人不是指自己,而是提升团队每个人的开发效率,这包括代码编写更快,编译更快,部署和发布更快,测试更方便等。
技能点:编译,全栈,流程管理
2、第二层交付:团队和公司效能
对于大型开发团队来讲,我们面临的是更复杂的开发环境,人员可能有几千,代码也能有几百万。这时候要提升团队的整体效率,需要从架构入手,搭建一套合适大型团队工作的代码架构。
目前客户端分Android和iOS两端,在业务侧基本是1:1人力配比,怎么更好的复用人力,目前在尝试的有各种跨端和动态化方案。这可以从更大维度来提升公司的效能。
技能点:架构设计,代码范式,跨端,大前端
\
自身- 打造自身的技术品牌
作为客户端研发,打造的产品就是App,通过App给用户提供价值,在这个过程中提升自己。所以从一定程度上说,自身是自己要打造的第二产品。对自身产品的交付也可以分成三个层次。
\
1、第一层交付:满足交付的基本技能
在这层的交付里面,我们要把自己锻炼为合适的客户端开发,能够满足上面所说的需求开发。特别是对于在校生或者校招生来讲,大部分人都是客户端零基础,需要尽快掌握最基本的研发技能,能承接需求,能了解与自己打交道的Android平台,这是最关键的交付。
2、第二层交付:打造自身的技术高度
让自己成为合格的需求承接只是第一步,要打开自身发展的天花板,需要培养自己的优势,打造自身的技术高度。如果在某个领域你可以达到行业前20%,那么你就有了更长期立身的资本;如果你能有两项技能可以达到行业前20%,那么你的天花板就比较高了。对于客户端研发来讲,打造技术高度可以围绕三个方面展开:
3、第三层交付:君子不器,培养自己的综合素养
如果再进一步呢?个人提升是没有止境的,有了可以傍身的技术实力,还需要进一步扩充自己的软素质。
3、认识Android 系统
接下来我们简单了解一下为了完成最简单交付需要掌握的基本技能。这一节我们介绍下Android系统,这是将来你打交道的主战场,下一节我们简单说下开发工具。
developer.android.com/guide/platf…
Android平台架构图
1、系统应用层
这一层就是各App所在的最上层了,我们自己开发的App和系统自带的App都在这一层,两种App本质上没有太大区别。一些系统的App提供的功能我们可以直接调用,比如打电话、发短信等,当然我们自己开发的app也可以给其他产品提供类似的调用功能。
2、 Java ****API 层
这层就是Android Framework提供给开发者的接口,我们可以基于这些接口打造各自的App。在这一层主要的技术栈就是最基础的交付内容,包括页面+逻辑+页面,一些多媒体相关的需求也有成熟的api可以直接使用。
3、原生 C/C++层
一些核心的系统服务和组件是C/C++编写的,我们可以用Android NDK 直接从原生代码访问某些原生平台库。从这一层往下,一些多样化的需求就可以被满足的很好,比如音视频编解码、安全、质量、体验等。
4、Android Runtime
这一层就会涉及虚拟机的知识,在一层会把DEX字节码进行编译,优化执行效率。在一层我们可以做一些体验相关的优化,让代码运行更高效。所需要的技术门槛也就更高一些。
5、硬件抽象层 (HAL)
主要提供硬件组件的封装,包括相机、传感器和蓝牙等。当框架 API 要求访问设备硬件时,Android 系统将为该硬件组件加载库模块。
6、Linux 内核
Android 平台的基础是 Linux 内核。例如,Android Runtime (ART) 依靠 Linux 内核来执行底层功能,例如线程和内存管理。
使用 Linux 内核可让 Android 利用主要安全功能,并且允许设备制造商为著名的内核开发硬件驱动程序。
\
APK的构成
- AndroidManifest.xml :生命app中四大组件,以及权限等
- classes.dex :所有编写的java、
- res文件夹 :资源文件夹,包括图片、颜色、字符串,以及搭建的XML布局文件
- META-INF文件夹:存在签名和证书,用于校验和安全
- lib文件夹:主要是存放C/C++代码编译成的so文件
\
\
4、认识工具
Android开发用的IDE是Android Studio,下载和配置直接参考官方文文档即可:
developer.android.google.cn/studio
\
5、认识Git
网上文章比较多,检索学习即可。
第二节:客户端基础知识必备
课程概述
-
基础组件
- Activity
- Fragment
- Service
- BroadcastReceiver
- ContentProvider
-
通信组件
- Handler
- Binder
课前
熟悉Android Studio基本用法
熟悉Java语言
课中
1 Android基础组件
1.1 Activity
Activity是用于展示数据,实现与用户的交互的容器。
1.1.1 Activity基本用法
1.1.2 Activity生命周期
onCreate()
:创建时回调,一般在此处创建视图和绑定数据
onStart()
:已启动,即将进入前台
onResume()
:与用户开始交互,位于Activity栈顶
onPause()
:Actvity失去焦点或已暂停,Activity界面部分可见,下一个生命周期是onResume(
)或onStop()
onStop()
:Activity不再可见,下一个回调是onRestart()
或onDestory()
onRestart()
:重启已停止的Activity,下一个回调是onStart()
onDestory()
:销毁Actvity,释放该Activity的所有资源
onSaveInstanceState()
:在非正常关闭时回调,用于保存数据,不支持持久化数据
onRestoreInstanceState()
/onCreate()
:用于恢复数据
常见场景下Activity生命周期流转:
1 启动:onCreate() - onStart() - OnResume() - Resumed 2 退出:Resumed - onPause() - onStop() - onDestroy() 3 部分覆盖:Resumed - onPause() - Paused 4 部分遮挡恢复:Paused - onResume() - Resumed 5 完全覆盖:Resumed - onPause() - onSaveInstanceState() - onStop() - Stoped 6 完全遮挡恢复:Stoped - onStart() - onResume() - Resumed 7 后台回收:Stoped - Killed 8 回收恢复:Killed - onCreate() - onStart() - onRestoreInstanceState()- onResume() - Resumed 9 配置改变:Resumed - onSaveInstanceState() - onPause() - onStop() - onDestroy() - onCteate() - onStart() - onRestoreInstanceState() - onResume()
1.1.3 Activity启动模式
- Standard 启动模式
- SingleTask 启动模式
- SingleTop 启动模式
- SingleInstance 启动模式
1.2 Fragment
1.2.1 Fragment基本用法
1.2.2 Fragment生命周期
可以看到 Fragment 的生命周期和 Activity 很相似,只是多了一下几个方法: onAttach() 在Fragment 和 Activity 建立关联是调用(Activity 传递到此方法内) onCreateView() 当Fragment 创建视图时调用 onActivityCreated() 在相关联的 Activity 的 onCreate() 方法已返回时调用。 onDestroyView() 当Fragment中的视图被移除时调用 onDetach() 当Fragment 和 Activity 取消关联时调用。
常用场景下生命周期流转:
1 启动:onAttach() - onCreate() - onCreateView() - onActivityCreated() - onStart() - onResume() - Resumed 2 退出:Resumed - onPause() - onStop() - onDestoryView() - onDestory() 3 部分覆盖:Resumed - onPause() - Paused 4 部分遮挡恢复:Paused - onResume() - Resumed 5 完全覆盖:Resumed - onPause() - onSaveInstanceState() - onStop() - Stoped 6 完全遮挡恢复:Stoped - onStart() - onResume() - Resumed 7 后台回收:Stoped - Killed 8 回收恢复:Killed - onCreate() - onStart() - onRestoreInstanceState()- onResume() - Resumed 注:Fragment生命周期可通过FragmentTransaction.setMaxLifecycle()手动干预
1.2.3 Fragment与Activity交互 juejin.cn/post/711501…
1.3 Service
1.4 Broadcast
1.5 ContentProvider
2 Android通信组件
2.1 Handler
2.2 Binder
课后
尝试仿照系统相册实现一个图库APP:
- 页面:相册页面 / 图片页面 / 大图页面
- 页面架构:Activity + Fragment
- 处理旋转屏幕场景,保证旋转屏幕页面不重建
- 内置自我升级能力(Service使用)
- 处理首页加载模式,避免页面栈中出现两个首页(SingleTask)
- 扫描系统所有图片(ContentProvider)
- 提供图片选择能力给系统(Intent)
参考资料
developer.android.com/training/ba…
developer.android.com/reference
第三节:常规 & 高级 UI 编程
1 课程概述
本课程将结合大量代码和示例逐步从单个UI组件基础到多个UI组件排版、从静态页面绘制到动态页面的设计、从系统组件应用到自定义组件等多个维度、由浅及深的阐述常规&高级UI编程相关知识。具体将分为以下几个部分:
- UI组件:学习Android UI组件相关知识
- 布局:学习如何将多个UI组件排版成想要的界面
- 渲染:学习Android UI渲染流程及原理
- 交互:学习Android常规的交互知识及原理
- 动画:学习Android动画相关知识
- 自定义View:学习如何自定义View
2 课前
- 利用Android Studio创建一个包含简单Activity的App,并正常运行起来
3 课中
3.1 UI组件
3.1.1 什么是Android UI?
- UI:User Interface
- Android系统是图形用户界面操作系统
- UI界面由多个不同功能的UI组件构成
- Android SDK提供了大量的UI组件
3.1.2 UI组件
常规UI组件大多由Android Framework中的android.widget这个package提供
常规View的属性和方法
3.1.3 UI组件间关系
3.2 布局
3.2.1 LinearLayout
LinearLayout示例
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:orientation="vertical" >
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="top" />
<Button
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_gravity="right" />
</LinearLayout>
3.2.2 RelativeLayout
RelativeLayout示例
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent"
android:paddingLeft="16dp" android:paddingRight="16dp" >
<EditText
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Spinner
android:id="@+id/dates"
android:layout_width="0dp" android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/times" />
<Spinner
android:id="@id/times"
android:layout_width="96dp" android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_alignParentRight="true" />
<Button
android:layout_width="96dp" android:layout_height="wrap_content"
android:layout_below="@id/times"
android:layout_alignParentRight="true" />
</RelativeLayout>
3.2.3 FrameLayout
FrameLayout示例
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<TextView
android:layout_width="300dp"
android:layout_height="300dp"
android:gravity="center"
android:background="@android:color/holo_blue_bright"
android:text="我是第一层"/>
<TextView
android:layout_width="150dp"
android:layout_height="140dp"
android:gravity="center"
android:background="@android:color/holo_green_light"
android:text="我是第二层"/>
</FrameLayout>
3.2.4 ConstraintLayout
ConstraintLayout示例
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_beauty"
android:layout_width="100dp"
android:layout_height="200dp"
android:scaleType="centerCrop"
android:src="@drawable/beauty"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_girl"
android:layout_width="100dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
android:src="@drawable/girl"
app:layout_constraintBottom_toBottomOf="@+id/iv_beauty"
app:layout_constraintLeft_toRightOf="@+id/iv_beauty"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
3.2.5 布局总结
3.3 渲染
3.3.1 布局加载
在Activity中设置布局文件
TextView textView;
@Override
public void onCreate(Bundle savedInstanceState) {
// call the super class onCreate to complete the creation of activity like
// the view hierarchy
super.onCreate(savedInstanceState);
// set the user interface layout for this activity
// the layout file is defined in the project res/layout/main_activity.xml file
setContentView(R.layout.main_activity);
// initialize member TextView so we can manipulate it later
textView = (TextView) findViewById(R.id.text_view);
}
setContentView究竟做了什么?通过源码分析可知,setContentView最终创建了DecorView,并由LayoutInflater来加载了XML文件
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor
.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
3.3.2 布局解析
布局加载后调用了LayoutInflater相关方法,那LayoutInflater究竟做了什么?通过源码分析可知,LayoutInflater解析了XML文件,并根据XML文件生成了View实例,并将View实例添加了到了其ViewGroup中
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
while (((type = parser.next()) != XmlPullParser.END_TAG||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
// 省略
// 核心代码
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
3.3.3 布局渲染
页面绘制流程
UI渲染流程
渲染流程
3.4 交互
3.4.1 常用交互事件监听器
3.4.2 触摸事件
当用户触摸屏幕时,系统将建立一系列的MotionEvent对象,MotionEvent包含关于发生触摸的位置和时间等细节信息,MotionEvent对象被传递到相应的捕获函数中,例如onTouchEvent()。
3.4.3 捕获触摸事件
- Activity和View都有onTouchEvent(),用于处理触摸事件。
- 当用户触摸屏幕时,会回调触摸视图上的onTouchEvent()。 对于最终被识别为手势的每个轻触事件序列,onTouchEvent() 都会多次被触发。
public class MainActivity extends Activity {
@Override
public boolean onTouchEvent(MotionEvent event){
int action = MotionEventCompat.getActionMasked(event);
switch(action) {
case (MotionEvent.ACTION_DOWN) :
Log.d(DEBUG_TAG,"Action was DOWN");
return true;
case (MotionEvent.ACTION_MOVE) :
Log.d(DEBUG_TAG,"Action was MOVE");
return true;
case (MotionEvent.ACTION_UP) :
Log.d(DEBUG_TAG,"Action was UP");
return true;
case (MotionEvent.ACTION_CANCEL) :
Log.d(DEBUG_TAG,"Action was CANCEL");
return true;
case (MotionEvent.ACTION_OUTSIDE) :
Log.d(DEBUG_TAG,"Movement occurred outside bounds " + "of current screen element");
return true;
default :
return super.onTouchEvent(event);
}
}
3.4.4 事件处理流程
3.4.5 交互总结
3.5 动画
3.5.1 帧动画
帧动画示例:
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/ic_wifi_0" android:duration="100"/>
<item android:drawable="@drawable/ic_wifi_1" android:duration="100"/>
<item android:drawable="@drawable/ic_wifi_2" android:duration="100"/>
<item android:drawable="@drawable/ic_wifi_3" android:duration="100"/>
<item android:drawable="@drawable/ic_wifi_4" android:duration="100"/>
<item android:drawable="@drawable/ic_wifi_5" android:duration="100"/>
</animation-list>
private void playAnimation() {
mImageView.setImageResource(R.drawable.frame_anim);
AnimationDrawable animationDrawable = (AnimationDrawable) mImageView.getDrawable();
animationDrawable.start();
...
animationDrawable.stop();
}
3.5.2 补间动画
补间动画示例:
public void tweenedAnimation(View view) {
// 创建一个透明度动画,透明度从1渐变至0
AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0);
alphaAnimation.setDuration(3000);
// 创建一个旋转动画,从0度旋转至360度
RotateAnimation rotateAnimation = new RotateAnimation(0, 360,
Animation.RELATIVE_TO_SELF, 0.5f,Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnimation.setDuration(3000);
ScaleAnimation scaleAnimation = new ScaleAnimation(1, 0.5f, 1, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setDuration(3000);
TranslateAnimation translateAnimation = new TranslateAnimation(
Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 1,
Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 1);
translateAnimation.setDuration(3000);
// 组合上述4种动画
AnimationSet animationSet = new AnimationSet(true);
animationSet.addAnimation(alphaAnimation);
animationSet.addAnimation(rotateAnimation);
animationSet.addAnimation(scaleAnimation);
animationSet.addAnimation(translateAnimation);
view.startAnimation(animationSet);
}
差值器示例:
3.5.3 属性动画
属性动画示例:
private void startObjectAnimatorSet() {
// 创建一个ObjectAnimator,将mImageView的scaleX属性值从1变化到0.5
Animator scaleXAnimator = ObjectAnimator.ofFloat(mImageView, "scaleX", 1, 0.5f);
scaleXAnimator.setDuration(2000);
// 创建一个ObjectAnimator,将mImageView的scaleY属性值从1变化到0.5
Animator scaleYAnimator = ObjectAnimator.ofFloat(mImageView, "scaleY", 1, 0.5f);
scaleYAnimator.setDuration(2000);
// 创建一个ObjectAnimator,将mImageView的rotationX属性值从0变化到360
Animator rotationXAnimator = ObjectAnimator.ofFloat(mImageView, "rotationX", 0, 360);
rotationXAnimator.setDuration(2000);
// 创建一个ObjectAnimator,将mImageView的rotationY属性值从0变化到360
Animator rotationYAnimator = ObjectAnimator.ofFloat(mImageView, "rotationY", 0, 360);
rotationYAnimator.setDuration(2000);
// 组合上述4种动画
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(scaleXAnimator).with(scaleYAnimator)
.before(rotationXAnimator).after(rotationYAnimator);
animatorSet.start();
}
3.5.4 动画总结
两类动画的根本区别在于:是否改变动画本身的属性
- 视图动画:不改变动画的属性,在动画过程中仅对图像进行变换来达到动画效果。无论动画结果在哪,该View的位置和响应区域都是在原地,不会根据结果而移动;
- 属性动画:改变了动画属性 因属性动画在动画过程中对动态改变了对象属性,从而达到了动画效果
3.6 自定义View
3.6.1 自定义View示例
创建View
public class SwitchButton extends View implements Checkable {
public SwitchButton(Context context) {
super(context);
init(context, null);
}
public SwitchButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public SwitchButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public SwitchButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
处理View布局
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if(widthMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.AT_MOST){
widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_WIDTH, MeasureSpec.EXACTLY);
}
if(heightMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.AT_MOST){
heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_HEIGHT, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
float viewPadding = Math.max(shadowRadius + shadowOffset, borderWidth);
height = h - viewPadding - viewPadding;
width = w - viewPadding - viewPadding;
...
}
绘制View
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制白色背景的圆角矩形
paint.setStrokeWidth(borderWidth);
paint.setStyle(Paint.Style.FILL);
paint.setColor(background);
drawRoundRect(canvas, left, top, right, bottom, viewRadius, paint);
//绘制关闭状态的边框
paint.setStyle(Paint.Style.STROKE);
paint.setColor(uncheckColor);
drawRoundRect(canvas,left, top, right, bottom, viewRadius, paint);
...
//绘制按钮左边绿色长条遮挡
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(1);
drawArc(canvas, left, top, left + 2 * viewRadius, top + 2 * viewRadius,90, 180, paint);
canvas.drawRect( left + viewRadius, top,viewState.buttonX,
top + 2 * viewRadius,paint);
...
//绘制按钮
drawButton(canvas, viewState.buttonX, centerY);
}
处理用户交互
@Override
public boolean onTouchEvent(MotionEvent event) {
if(!isEnabled()) { return false; }
switch (actionMasked){
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_MOVE:
if(isPendingDragState()){ //在准备进入拖动状态过程中,可以拖动按钮位置
...
}else if(isDragState()){ //拖动按钮位置,同时改变对应的背景颜色
...
}
break;
case MotionEvent.ACTION_UP:
if(System.currentTimeMillis() - touchDownTime <= 300){ //点击时间小于300ms,认为是点击操作
toggle();
}else if(isDragState()){ //在拖动状态,计算按钮位置,设置是否切换状态
...
}
break;
case MotionEvent.ACTION_CANCEL:
removeCallbacks(postPendingDrag);
break;
}
return true;
}
处理动画
// 初始化View时设置动画
valueAnimator = ValueAnimator.ofFloat(0f, 1f);
valueAnimator.setDuration(effectDuration);
valueAnimator.setRepeatCount(0);
valueAnimator.addUpdateListener(animatorUpdateListener);
// 点击开关后启动动画
valueAnimator.start();
// 简单动画更新回调,触发View绘制
private ValueAnimator.AnimatorUpdateListener animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
switch (animateState) {
...
case ANIMATE_STATE_SWITCH:
viewState.buttonX = beforeState.buttonX + (afterState.buttonX - beforeState.buttonX) * value;
float fraction = (viewState.buttonX - buttonMinX) / (buttonMaxX - buttonMinX);
viewState.checkStateColor = (int) argbEvaluator.evaluate( fraction,uncheckColor,checkedColor);
viewState.radius = fraction * viewRadius;
viewState.checkedLineColor = (int) argbEvaluator.evaluate(fraction,Color.TRANSPARENT, checkLineColor);
break;
}
postInvalidate();
}
};
3.6.2 自定义View小结
3.7 课程总结
4 课后
- 编写一个包含自定义View、动画和多种交互方式的复杂UI界面
5 参考资料
- 自定义View:github.com/zcweng/Swit…