【Android 客户端专场 学习资料一】第四届字节跳动青训营

4,417 阅读16分钟

第四届字节跳动青训营讲师非常用心给大家整理了课前、中、后的学习内容,同学们自我评估,选择性查漏补缺,便于大家更好的跟上讲师们的节奏,祝大家学习愉快,多多提问交流~

第一节: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基本用法

juejin.cn/post/684490…

1.1.2 Activity生命周期

juejin.cn/post/684490…

  • 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启动模式

juejin.cn/post/684490…

  • Standard 启动模式

  • SingleTask 启动模式

  • SingleTop 启动模式

  • SingleInstance 启动模式

1.2 Fragment

juejin.cn/post/684490…

1.2.1 Fragment基本用法

juejin.cn/post/684490…

1.2.2 Fragment生命周期

juejin.cn/post/705408…

可以看到 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

juejin.cn/post/684490…

1.4 Broadcast

juejin.cn/post/705216…

1.5 ContentProvider

juejin.cn/post/699330…

2 Android通信组件

2.1 Handler

juejin.cn/post/684490…

2.2 Binder

juejin.cn/post/684490…

课后

尝试仿照系统相册实现一个图库APP:

参考:github.com/pengjianbo/…

  1. 页面:相册页面 / 图片页面 / 大图页面

  1. 页面架构:Activity + Fragment
  1. 处理旋转屏幕场景,保证旋转屏幕页面不重建
  1. 内置自我升级能力(Service使用)
  1. 处理首页加载模式,避免页面栈中出现两个首页(SingleTask)
  1. 扫描系统所有图片(ContentProvider)
  1. 提供图片选择能力给系统(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示例

github.com/zcweng/Swit…

创建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 参考资料