Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Kotlin学习之-5.2 属性和成员

$
0
0

Kotlin学习之-5.2 属性和成员

定义属性

Kotlin中,类可以有属性。它们既可以用var关键字定义成变量,也可以用val关键字定义成只读量。

class Address {
    var name: String = ...
    var street: String = ...
    var city: String = ...
    var state: String? = ...
    var zip: String = ...
}

使用一个属性,只要简单的用它的名字引用即可,就像Java中的成员一样。

fun copyAddress(address: Address): Address {
    val result = Address() // Kotlin中没有'new' 关键字
    result.name = address.name
    result.street = address.street
    //...
    return result
}

Getter 和Setter

定义属性完整语法结构如下:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

构造器、getter 和setter都是可选项。如果属性的类型可以从构造器中推断出来的话,那么属性的类型也是可选的。示例如下:

var allByDefault: Int? // error: 需要显式的构造器。 有默认的getter 和setter
var initialized = 1 // 类型是Int。   有默认的getter 和setter

只读属性的语法和变量属性的的语法有两点不同: 定义是用val 而不是var,并且没有setter 方法

val simple: Int? // 类型是Int,默认的getter方法,必须在构造函数中初始化。
val inferredType = 1 // 类型是Int, 默认的getter方法。

我们可以在属性定义时自定义getter方法,和普通的函数非常像。示例如下:

val isEmpty: Boolean
    get() = this.size == 0

自定义setter方法示例如下:

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // 解析字符串并且赋值给其他属性
    }

一般情况,setter方法的参数名是value,但是也可以选择一个其他名字。
从Kotlin v1.1开始,如果属性类型可以从getter方法中推断出来,那么定义的时候可以省略属性类型。

val isEmpty() get() = this.size == 0 // 推断的类型Boolean

如果要更改函数的可见性或者添加注解,但是不需要干煸默认的实现,可以定义访问函数但不定义函数主体。

var setterVisibility: String = "abc"
    private set

var setterWithAnnotation: Any? = null
    @inject set

Backing Fields

Kotlin中,类没有成员。但是有时候使用自定义访问函数时也需要一个’backing field’。 在这些用法中,Kotlin提供一个自动的’backing field’, 它可以用field关键字来访问。

var counter = 0
    set(value) {
        if (value >= 0) field = value
    }

field描述符只能用在属性的访问。

属性会产生一个’backing field’,如果属性有至少一个默认实现的访问函数,或者自定义的访问函数使用了field描述符来访问这个属性。
例如,下面的例子中没有没有’backing field’

val isEmpty: Boolean
    get() = this.size == 0

Backing Properties

如果你需要做的事情不符合隐式backing field模式,那么可以使用backing property

private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap() // 类型参数是推断出来的
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }

总的来说,这和Java是一样的,使用默认的getter和setter方法访问私有的属性会被优化,从而没有函数调用的负担。

编译期常量

属性的值在编译期就确定的话,用const修饰符来表示成编译期常量。这样的属性需要满足如下要求。

  • 顶级的或者是一个object的成员
  • 是用String类型或者基础类型初始化的
  • 没有自定义getter方法

这样的属性可以在注解中使用

const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }

延迟初始化的属性

一般情况,被定义成非空的属性必须在构造函数中初始化。但是,这样通常不太方便。例如,属性可以在依赖注入的时候被初始化,或者在单元测试的setup方法中初始化。 在这种情况,就不能再构造函数中提供一个非空的初始化器,但是你仍然想要在引用属性的时候避免空指针检查。

处理这种情况,需要用lateinit 描述符来标识这个属性。

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()
    }
}

延迟初始化lateinit 描述符只能用在var属性,并且属性不能有自定义的getter和setter。属性的类型必须是非空的,并且不能是基础类型。

在属性初始化之前,访问一个延迟初始化lateinit的属性,会抛出一个特殊的异常,该异常表明属性在访问的时候,还没有初始化。

代理属性

最常用的属性就是简单地读取或者写入backing field。 另外,使用自定义的getter和setter可以实现属性任意的操作。 除了这两种情况,还有一些常见的用法。例如,延迟初始化的值,用一个给定的关键字从一个map中获取对应的值,访问数据库,通知监听器等等。

这些常用的行为和操作可以通过库的方式来实现。这可以使用代理属性的方式来实现。


PS,我会坚持把这个系列写完,有问题可以留言交流,也关注专栏Kotlin for Android Kotlin安卓开发

作者:farmer_cc 发表于2017/6/12 15:30:43 原文链接
阅读:245 评论:0 查看评论

Kotlin学习记录(五)—— 条件表达式的使用

$
0
0


接上篇: Kotlin学习记录(四)—— 常用集合的使用


if/else:


直接上代码吧:

var x: Int = 666
var y:Int
if(x>0){
    y=x
}else{
    y=0
}
是的,和Java没啥区别!真的没啥区别吗?那这样呢:


val z=if(x>0) x else 0

觉得还不够简单?那这样呢:


val a= x ?: x ?: 0

我肯定会告诉你y,z,a的值都是一样的!自行体会下,第三种写法和Java的三元表达式是有区别的哈。

when:


when相当于Java中的switch/case,但是要比其要更强大。这个表达式会去试图匹配所有可能的分支直到找到满意的一项。与Java不同的是,它的条件可以是任何类型的,并且也可以是一个条件。

when(x){
    666 -> Log.e("log","x=666,则输出这句话")
    111,222 -> Log.e("log","x=111或者=222,则输出这句话")
    else -> Log.e("log","如果条件都不满足,则输出这句话")
}
val view:View?=null

when(view){
    is TextView ->  Log.e("log","view如果是TextView,则输出这句话")
    is ImageView -> Log.e("log","view如果是ImageView,则输出这句话")
    else -> {       
	view?.visibility=View.GONE
Log.e("log","执行代码当然也可以是代码块") }}

那么再看看下边的:

val value:Int=when{
    x>0 -> x
    x<0 -> y
    "abcdef".contains("bcd") -> 666
    else -> 0
}

在kotlin中一切都是表达式,也就是一切都是有返回值的,包括when这样的条件表达式,所以可以直接给value赋值。同时通过没有给when传参数,实现了各种条件判断的条件。就是这么任性。


For:

kotlin中for循环需要通过迭代器进行迭代:

for(item in listIterator){
    Log.i("log",item)
}

迭代器listIterator的获取,可参考上篇文章。如果是遍历list或者array可以通过索引完成:

for(i in list.indices){
    Log.i("log",list[i])
}


while和do/while:

while (x>0){
    x--
}

do{
    x--
}while (x>0)


返回/跳转:


在Kotlin中,返回跳转符的使用和Java基本相同:

return:指定方法返回什么值,并且方法结束。

break:结束最近的闭合循环,不执行循环中剩余的语句。

continue:跳到最近的闭合循环的下一次循环。









作者:u011732740 发表于2017/6/12 17:18:14 原文链接
阅读:207 评论:0 查看评论

Flutter实战一Flutter聊天应用(七)

$
0
0

使用Firebase控制台的分析(Analytics)功能可以帮助我们了解用户是如何使用Flutter应用程序。我们将启用捕获预定义的事件,调整应用程序以收集登录事件和发送消息的指标。数据捕获后,我们将通过Firebase控制台在仪表板中查看。

要使用Firebase Analytics收集用户的数据,我们需要firebase_analytics插件。在main.dart文件中,确保导入相应的包。

import 'package:firebase_analytics/firebase_analytics.dart';

现在添加一个名为analytics的私有成员变量,使用一个新的FirebaseAnalytics实例初始化它,我们可以通过此变量进行访问。将以下代码添加到main.dart中。

final analytics = new FirebaseAnalytics();

接下来,我们可以在应用程序中记录一些事件,以便稍后跟踪和分析。对于这个项目,我们将跟踪用户使用其Google帐户登录的次数。我们将通过在ChatScreenState中的私有_ensureLoggedIn()方法中记录登录事件来进行此操作,当用户发送聊天消息时,该事件被调用。

class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
  //...
  Future<Null> _ensureLoggedIn() async {
    GoogleSignInAccount user = googleSignIn.currentUser;
    if (user == null)
      user = await googleSignIn.signInSilently();
    if (user == null) {
      await googleSignIn.signIn();
      analytics.logLogin();
    }
  }
  //...
}

调用logLogin()方法,该方法由应用程序中包含的Flutter Firebase Analytics插件定义。此方法不带参数,记录名为login的Firebase Analytics事件。

我们还可以跟踪应用程序中用户发送的消息数量,作为使用和受欢迎程度的度量标准。用户安装我们的应用程序后,重要的是要知道他们是否觉得它很有用和有吸引力。我们将通过在ChatScreenState中的私有_sendMessage()方法中记录发送的消息事件来执行此操作。

class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
  //...
  void _sendMessage({ String text }) {
    ChatMessage message = new ChatMessage(
      text: text,
      animationController: new AnimationController(
        duration: new Duration(milliseconds: 300),
        vsync: this
      )
    );
    setState((){
      _messages.insert(0, message);
    });
    message.animationController.forward();
    analytics.logEvent(name: 'send_message');
  }
  //...
}

调用由Firebase Analytics API定义的logEvent()方法,访问此API由Flutter Firebase Analytics插件提供,该插件是我们之前导入的。logEvent()方法记录了名为send_message的Firebase Analytics事件。

现在当用户登录应用程序或发送消息时,事件将记录在Firebase实时数据库中。在Firebase控制台中,选择“Analytics > 事件”查看仪表板。

这里写图片描述

在事件列表中,我们将看到刚刚添加的loginsend_message事件以及默认的first_openscreen_viewsession_start事件。我们登录到Firebase Analytics的任何事件将被汇总、匿名化,并在24小时内在Firebase控制台中报告。要立即查看事件,可以启用调试模式。

Firebase认证允许我们要求应用程序的用户拥有Google帐户。当用户登录时,Firebase身份验证将验证Google登录中的凭据,并返回应用程序的响应。登录和验证的用户可以连接到Firebase实时数据库,并与其他用户交流聊天消息。我们可以应用身份验证,以确保用户只能看到他们可以访问的消息。

要使用Firebase认证来验证应用程序的用户,我们需要使用firebase_auth插件。在我们的main.dart文件中,确保导入相应的包。

import 'package:firebase_auth/firebase_auth.dart';

现在添加一个名为auth的私有成员变量,使用一个新的FirebaseAuth实例初始化它,我们可以通过此变量进行访问。将以下代码添加到main.dart中。

final auth = FirebaseAuth.instance;

将Google登录连接到Firebase,要求仅登录的Google用户可以更改数据库,需要在ChatScreenState中的_ensureLoggedIn()方法中添加验证逻辑,如下面代码所示。

class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
  //...
  Future<Null> _ensureLoggedIn() async {
    GoogleSignInAccount user = googleSignIn.currentUser;
    if (user == null)
      user = await googleSignIn.signInSilently();
    if (user == null) {
      await googleSignIn.signIn();
      analytics.logLogin();
    }
    if (auth.currentUser == null) {
      GoogleSignInAuthentication credentials = await googleSignIn.currentUser.authentication;
      await auth.signInWithGoogle(
        idToken: credentials.idToken,
        accessToken: credentials.accessToken,
      );
    }
  }
  //...
}

检查currentUser是否设置为nullauthentication属性是用户的凭据。signInWithGoogle()方法将idTokenaccessToken作为参数。此方法由我们之前导入的Flutter Firebase Authentication插件提供。它返回一个名为currentUser的新的Firebase用户对象。

这里写图片描述

我们现在可以在Firebase控制台的Authentication中看到自己的帐户信息。现在,我们可以要求用户使用Google帐户登录,然后才能发送消息。

作者:hekaiyou 发表于2017/6/12 17:58:00 原文链接
阅读:184 评论:0 查看评论

Tiny4412 Android5.0 定制键值相关的文件

$
0
0

在4412中,开发板上只有四个按键,我们可以根据产品的需求自定义按键的功能。

在KeyEvent.java这个文件中,可以通过发出按键的事件,上报给上层。让上层app或者服务去接收处理。比如,我们可以看到这样的代码:

注释写得非常清楚,按下按键后,可以发出键值,按下的时间等等,也可以通过Android广播的形式去发送这些事件。

    /**
     * Create a new key event.
     *
     * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
     * at which this key code originally went down.
     * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
     * at which this event happened.
     * @param action Action code: either {@link #ACTION_DOWN},
     * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
     * @param code The key code.
     * @param repeat A repeat count for down events (> 0 if this is after the
     * initial down) or event count for multiple events.
     * @param metaState Flags indicating which meta keys are currently pressed.
     * @param deviceId The device ID that generated the key event.
     * @param scancode Raw device scan code of the event.
     */
    public KeyEvent(long downTime, long eventTime, int action,
                    int code, int repeat, int metaState,
                    int deviceId, int scancode) {
        mDownTime = downTime;
        mEventTime = eventTime;
        mAction = action;
        mKeyCode = code;
        mRepeatCount = repeat;
        mMetaState = metaState;
        mDeviceId = deviceId;
        mScanCode = scancode;
    }
那么,如果我们要修改一个按键做别的功能,我们可以修改按键的KeyCode。

接下来,我们看到这个文件开头定义了这么一个类:

public class KeyEvent extends InputEvent implements Parcelable {
    /** Key code constant: Unknown key code. */
    public static final int KEYCODE_UNKNOWN         = 0;
    /** Key code constant: Soft Left key.
     * Usually situated below the display on phones and used as a multi-function
     * feature key for selecting a software defined function shown on the bottom left
     * of the display. */
    public static final int KEYCODE_SOFT_LEFT       = 1;
    /** Key code constant: Soft Right key.
     * Usually situated below the display on phones and used as a multi-function
     * feature key for selecting a software defined function shown on the bottom right
     * of the display. */
    public static final int KEYCODE_SOFT_RIGHT      = 2;
    /** Key code constant: Home key.
     * This key is handled by the framework and is never delivered to applications. */
    public static final int KEYCODE_HOME            = 3;
    /** Key code constant: Back key. */
    public static final int KEYCODE_BACK            = 4;
    /** Key code constant: Call key. */
    public static final int KEYCODE_CALL            = 5;

.......
}
几乎所有相关的键值都被定义在这里,可以通过这个来修改键值或者添加键值。

该文件位于: /framework/base/core/java/android/view/KeyEvent.java

当然如果需要添加一个键值,那么还需要在这个文件中声明一个键值的枚举:

该文件位于:

/framwork/native/include/android/keycode.h 

例如会看到以下代码:

#include <sys/types.h>

#ifdef __cplusplus
extern "C" {
#endif

/*
 * Key codes.
 */
enum {
    AKEYCODE_UNKNOWN         = 0,
    AKEYCODE_SOFT_LEFT       = 1,
    AKEYCODE_SOFT_RIGHT      = 2,
    AKEYCODE_HOME            = 3,
    AKEYCODE_BACK            = 4,
    AKEYCODE_CALL            = 5,
    AKEYCODE_ENDCALL         = 6,
    AKEYCODE_0               = 7,
    AKEYCODE_1               = 8,
    AKEYCODE_2               = 9,
    AKEYCODE_3               = 10,
    AKEYCODE_4               = 11,
    AKEYCODE_5               = 12,
    AKEYCODE_6               = 13,
    AKEYCODE_7               = 14,
.....
};
需要添加键值,就往下添加一个就行了,然后需要在一个xml文件里声明一下,这是Android的一个布局文件。

文件位于:/framework/base/core/res/res/values/attrs.xml

在文件大概1512行的地方会看到以下信息:

   <!-- ========================== -->
    <!-- Key Codes                  -->
    <!-- ========================== -->
    <eat-comment />

    <!-- This enum provides the same keycode values as can be found in
        {@link android.view.KeyEvent}. -->
    <attr name="keycode">
        <enum name="KEYCODE_UNKNOWN" value="0" />
        <enum name="KEYCODE_SOFT_LEFT" value="1" />
        <enum name="KEYCODE_SOFT_RIGHT" value="2" />
        <enum name="KEYCODE_HOME" value="3" />
        <enum name="KEYCODE_BACK" value="4" />
        <enum name="KEYCODE_CALL" value="5" />
        <enum name="KEYCODE_ENDCALL" value="6" />
        <enum name="KEYCODE_0" value="7" />
        <enum name="KEYCODE_1" value="8" />
        <enum name="KEYCODE_2" value="9" />
        <enum name="KEYCODE_3" value="10" />
        <enum name="KEYCODE_4" value="11" />
        <enum name="KEYCODE_5" value="12" />
        <enum name="KEYCODE_6" value="13" />
        <enum name="KEYCODE_7" value="14" />
        <enum name="KEYCODE_8" value="15" />
        <enum name="KEYCODE_9" value="16" />
        <enum name="KEYCODE_STAR" value="17" />
        <enum name="KEYCODE_POUND" value="18" />
        <enum name="KEYCODE_DPAD_UP" value="19" />
        <enum name="KEYCODE_DPAD_DOWN" value="20" />
        <enum name="KEYCODE_DPAD_LEFT" value="21" />
.......
这些enum name就是刚刚在keycode.h里的枚举变量与value的声明。

那么Android又是怎么知道我按下的这个按键具体是什么按键呢?按键发出的事件为什么是这个值呢?所以,在这里就需要有一个Linux内核与Android的映射文件了。

通常,如果厂商没有定制这个映射文件,就会默认去使用:

/framework/base/data/keyboards/Generic.kl 这个文件。

但,在tiny4412中,友善之臂已经做了键值映射,这个文件就是tiny4412-key.kl

位于: device/friendly-arm/tiny4412/tiny4412-key.kl

我们可以来看看这个文件:

# Copyright (C) 2010 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

key 158   BACK
key 230   SOFT_RIGHT
key 60    SOFT_RIGHT
key 107   ENDCALL
key 62    ENDCALL
key 229   MENU
key 139   MENU
key 59    MENU
key 127   SEARCH
key 217   SEARCH
key 228   POUND
key 227   STAR
key 231   CALL
key 61    CALL
key 232   DPAD_CENTER
key 108   DPAD_DOWN
key 103   DPAD_UP
key 102   HOME
key 105   DPAD_LEFT
key 106   DPAD_RIGHT
key 115   VOLUME_UP
key 114   VOLUME_DOWN
key 116   POWER
key 212   CAMERA
key 353   DPAD_CENTER
对应的,我们也可以找到kernel/include/linux/input.h中相关的键值定义:

这个文件里的键值就可以找到对应kl文件的键值。

大概在源码的186行可以看到:

/*
 * Keys and buttons
 *
 * Most of the keys/buttons are modeled after USB HUT 1.12
 * (see http://www.usb.org/developers/hidpage).
 * Abbreviations in the comments:
 * AC - Application Control
 * AL - Application Launch Button
 * SC - System Control
 */

#define KEY_RESERVED		0
#define KEY_ESC			1
#define KEY_1			2
#define KEY_2			3
#define KEY_3			4
#define KEY_4			5
#define KEY_5			6
#define KEY_6			7
#define KEY_7			8
#define KEY_8			9
#define KEY_9			10
#define KEY_0			11
#define KEY_MINUS		12
#define KEY_EQUAL		13
#define KEY_BACKSPACE		14
#define KEY_TAB			15
#define KEY_Q			16
#define KEY_W			17
#define KEY_E			18
#define KEY_R			19
#define KEY_T			20
#define KEY_Y			21
#define KEY_U			22
#define KEY_I			23
#define KEY_O			24
#define KEY_P			25
#define KEY_LEFTBRACE		26
#define KEY_RIGHTBRACE		27
#define KEY_ENTER		28
#define KEY_LEFTCTRL		29
#define KEY_A			30
#define KEY_S			31
#define KEY_D			32
#define KEY_F			33
#define KEY_G			34
#define KEY_H			35
#define KEY_J			36
#define KEY_K			37
#define KEY_L			38
#define KEY_SEMICOLON		39
#define KEY_APOSTROPHE		40
#define KEY_GRAVE		41
#define KEY_LEFTSHIFT		42
#define KEY_BACKSLASH		43
#define KEY_Z			44
#define KEY_X			45
#define KEY_C			46
#define KEY_V			47
#define KEY_B			48
#define KEY_N			49
#define KEY_M			50
#define KEY_COMMA		51
#define KEY_DOT			52
#define KEY_SLASH		53
#define KEY_RIGHTSHIFT		54
#define KEY_KPASTERISK		55
#define KEY_LEFTALT		56
#define KEY_SPACE		57
#define KEY_CAPSLOCK		58
#define KEY_F1			59
#define KEY_F2			60
#define KEY_F3			61
#define KEY_F4			62
#define KEY_F5			63
#define KEY_F6			64
#define KEY_F7			65
#define KEY_F8			66
#define KEY_F9			67
#define KEY_F10			68
#define KEY_NUMLOCK		69
#define KEY_SCROLLLOCK		70
#define KEY_KP7			71
#define KEY_KP8			72
#define KEY_KP9			73
#define KEY_KPMINUS		74
#define KEY_KP4			75
#define KEY_KP5			76
#define KEY_KP6			77
#define KEY_KPPLUS		78
#define KEY_KP1			79
#define KEY_KP2			80
#define KEY_KP3			81
#define KEY_KP0			82
#define KEY_KPDOT		83

#define KEY_ZENKAKUHANKAKU	85
#define KEY_102ND		86
#define KEY_F11			87
#define KEY_F12			88
#define KEY_RO			89
#define KEY_KATAKANA		90
#define KEY_HIRAGANA		91
#define KEY_HENKAN		92
#define KEY_KATAKANAHIRAGANA	93
#define KEY_MUHENKAN		94
#define KEY_KPJPCOMMA		95
#define KEY_KPENTER		96
#define KEY_RIGHTCTRL		97
#define KEY_KPSLASH		98
#define KEY_SYSRQ		99
#define KEY_RIGHTALT		100
#define KEY_LINEFEED		101
#define KEY_HOME		102
#define KEY_UP			103
#define KEY_PAGEUP		104
#define KEY_LEFT		105
#define KEY_RIGHT		106
#define KEY_END			107
#define KEY_DOWN		108
#define KEY_PAGEDOWN		109
#define KEY_INSERT		110
#define KEY_DELETE		111
#define KEY_MACRO		112
#define KEY_MUTE		113
#define KEY_VOLUMEDOWN		114
#define KEY_VOLUMEUP		115
#define KEY_POWER		116	/* SC System Power Down */
#define KEY_KPEQUAL		117
#define KEY_KPPLUSMINUS		118
#define KEY_PAUSE		119
#define KEY_SCALE		120	/* AL Compiz Scale (Expose) */

#define KEY_KPCOMMA		121
#define KEY_HANGEUL		122
#define KEY_HANGUEL		KEY_HANGEUL
#define KEY_HANJA		123
#define KEY_YEN			124
#define KEY_LEFTMETA		125
#define KEY_RIGHTMETA		126
#define KEY_COMPOSE		127

#define KEY_STOP		128	/* AC Stop */
#define KEY_AGAIN		129
#define KEY_PROPS		130	/* AC Properties */
#define KEY_UNDO		131	/* AC Undo */
#define KEY_FRONT		132
#define KEY_COPY		133	/* AC Copy */
#define KEY_OPEN		134	/* AC Open */
#define KEY_PASTE		135	/* AC Paste */
#define KEY_FIND		136	/* AC Search */
#define KEY_CUT			137	/* AC Cut */
#define KEY_HELP		138	/* AL Integrated Help Center */
#define KEY_MENU		139	/* Menu (show menu) */
#define KEY_CALC		140	/* AL Calculator */
#define KEY_SETUP		141
#define KEY_SLEEP		142	/* SC System Sleep */
#define KEY_WAKEUP		143	/* System Wake Up */
#define KEY_FILE		144	/* AL Local Machine Browser */
#define KEY_SENDFILE		145
#define KEY_DELETEFILE		146
#define KEY_XFER		147
#define KEY_PROG1		148
#define KEY_PROG2		149
#define KEY_WWW			150	/* AL Internet Browser */
#define KEY_MSDOS		151
#define KEY_COFFEE		152	/* AL Terminal Lock/Screensaver */
#define KEY_SCREENLOCK		KEY_COFFEE
#define KEY_DIRECTION		153
#define KEY_CYCLEWINDOWS	154
#define KEY_MAIL		155
#define KEY_BOOKMARKS		156	/* AC Bookmarks */
#define KEY_COMPUTER		157
#define KEY_BACK		158	/* AC Back */
#define KEY_FORWARD		159	/* AC Forward */
#define KEY_CLOSECD		160
#define KEY_EJECTCD		161
#define KEY_EJECTCLOSECD	162
#define KEY_NEXTSONG		163
#define KEY_PLAYPAUSE		164
#define KEY_PREVIOUSSONG	165
#define KEY_STOPCD		166
#define KEY_RECORD		167
#define KEY_REWIND		168
#define KEY_PHONE		169	/* Media Select Telephone */
#define KEY_ISO			170
#define KEY_CONFIG		171	/* AL Consumer Control Configuration */
#define KEY_HOMEPAGE		172	/* AC Home */
#define KEY_REFRESH		173	/* AC Refresh */
#define KEY_EXIT		174	/* AC Exit */
#define KEY_MOVE		175
#define KEY_EDIT		176
#define KEY_SCROLLUP		177
#define KEY_SCROLLDOWN		178
#define KEY_KPLEFTPAREN		179
#define KEY_KPRIGHTPAREN	180
#define KEY_NEW			181	/* AC New */
#define KEY_REDO		182	/* AC Redo/Repeat */

#define KEY_F13			183
#define KEY_F14			184
#define KEY_F15			185
#define KEY_F16			186
#define KEY_F17			187
#define KEY_F18			188
#define KEY_F19			189
#define KEY_F20			190
#define KEY_F21			191
#define KEY_F22			192
#define KEY_F23			193
#define KEY_F24			194

#define KEY_SYM			198
#define KEY_CENTER		199

#define KEY_PLAYCD		200
#define KEY_PAUSECD		201
#define KEY_PROG3		202
#define KEY_PROG4		203
#define KEY_DASHBOARD		204	/* AL Dashboard */
#define KEY_SUSPEND		205
#define KEY_CLOSE		206	/* AC Close */
#define KEY_PLAY		207
#define KEY_FASTFORWARD		208
#define KEY_BASSBOOST		209
#define KEY_PRINT		210	/* AC Print */
#define KEY_HP			211
#define KEY_CAMERA		212
#define KEY_SOUND		213
#define KEY_QUESTION		214
#define KEY_EMAIL		215
#define KEY_CHAT		216
#define KEY_SEARCH		217
#define KEY_CONNECT		218
#define KEY_FINANCE		219	/* AL Checkbook/Finance */
#define KEY_SPORT		220
#define KEY_SHOP		221
#define KEY_ALTERASE		222
#define KEY_CANCEL		223	/* AC Cancel */
#define KEY_BRIGHTNESSDOWN	224
#define KEY_BRIGHTNESSUP	225
#define KEY_MEDIA		226

#define KEY_SWITCHVIDEOMODE	227	/* Cycle between available video
					   outputs (Monitor/LCD/TV-out/etc) */
#define KEY_KBDILLUMTOGGLE	228
#define KEY_KBDILLUMDOWN	229
#define KEY_KBDILLUMUP		230

#define KEY_SEND		231	/* AC Send */
#define KEY_REPLY		232	/* AC Reply */
#define KEY_FORWARDMAIL		233	/* AC Forward Msg */
#define KEY_SAVE		234	/* AC Save */
#define KEY_DOCUMENTS		235

#define KEY_BATTERY		236

#define KEY_BLUETOOTH		237
#define KEY_WLAN		238
#define KEY_UWB			239

#define KEY_UNKNOWN		240

#define KEY_VIDEO_NEXT		241	/* drive next video source */
#define KEY_VIDEO_PREV		242	/* drive previous video source */
#define KEY_BRIGHTNESS_CYCLE	243	/* brightness up, after max is min */
#define KEY_BRIGHTNESS_ZERO	244	/* brightness off, use ambient */
#define KEY_DISPLAY_OFF		245	/* display device to off state */

#define KEY_WIMAX		246
#define KEY_RFKILL		247	/* Key that controls all radios */

#define KEY_POUND		248
#define KEY_STAR		249
#define KEY_NETWORK		250

#define KEY_FOLDER_OPEN		251  /*only use Grande CHN CTC */
#define KEY_FOLDER_CLOSE	252  /*only use Grande CHN CTC */
#define KEY_3G	253  /*only use Grande CHN CTC */

/* Dummy touchkey code */
#define KEY_DUMMY_HOME1		249
#define KEY_DUMMY_HOME2		250
#define KEY_DUMMY_MENU		251
#define KEY_DUMMY_HOME		252
#define KEY_DUMMY_BACK		253
/* kona dummy touchkey */
#define KEY_DUMMY_1     251
#define KEY_DUMMY_2     252
#define KEY_DUMMY_3     253
(完)




作者:morixinguan 发表于2017/6/12 21:15:50 原文链接
阅读:324 评论:0 查看评论

iOS之Runtime原理解读

$
0
0

Runtime简介

做过Android开发的同学都知道,早期的Android系统采用的是Dalvik机制,应用每次运行的时候,字节码都需要通过即时编译器转换为机器码,大大的降低了app的运行效率。在Android 5.0系统之后,系统采用了ART机制,应用在第一次安装的时候,字节码就会预先编译成机器码,以后每次运行速度大大的提高了。

OC是一门动态语言,所以它总是想办法把一些决定工作从编译推迟到运行时,也就是说在iOS的编译系统里,光有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的工作。

iOS系统采用的就是Runtime机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC函数来说,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

基本结构

要谈Runtime机制,必然要先了解OC的对象以及类的结构。首先我们看一下和Runtime相关的头文件。
这里写图片描述
和运行时相关的头文件,其中主要使用的函数定义在message.h和runtime.h这两个文件中。在message.h中主要包含了一些向对象发送消息的函数,这是OC对象方法调用的底层实现。使用时只需要导入头文件即可。

#import <objc/message.h>
#import <objc/runtime.h>

runtime.h是运行时最重要的文件,其中包含了对运行时进行操作的方法。 主要包括:

操作对象的类型的定义

/// An opaque type that represents a method in a class definition. 一个类型,代表着类定义中的一个方法
typedef struct objc_method *Method;

/// An opaque type that represents an instance variable.代表实例(对象)的变量
typedef struct objc_ivar *Ivar;

/// An opaque type that represents a category.代表一个分类
typedef struct objc_category *Category;

/// An opaque type that represents an Objective-C declared property.代表OC声明的属性
typedef struct objc_property *objc_property_t;

// Class代表一个类,它在objc.h中这样定义的  typedef struct objc_class *Class;
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

这些类型的定义,对一个类进行了完全的分解,将类定义或者对象的每一个部分都抽象为一个类型type,对操作一个类属性和方法非常方便。OBJC2_UNAVAILABLE标记的属性是Ojective-C 2.0不支持的,但实际上可以用响应的函数获取这些属性。

对于上面的源码,有几个字段需要说明:
isa:这里的isa指针同样是一个指向objc_class的指针,表明该Class的类型,这里的isa指针指向的就是我们常说的meta-class了。不难看出,类本身也是一个对象。
super_class:这个指针就是指向该class的super class,即指向父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。
objc_method_list: 方法链表中存放的是该类的成员方法(-方法),类方法(+方法)存在meta-class的objc_method_list链表中。

通过图来描述相应的继承关系如下:
这里写图片描述
说明:
所有metaclass中isa指针都指向跟metaclass,而跟metaclass则指向自身。
Root metaclass是通过继承Root class产生的,与root class结构体成员一致,也就是前面提到的结构。
不同的是Root metaclass的isa指针指向自身。
root class的super class 指向的是nil。

函数的定义

函数的定义规则如下:

  • 对对象进行操作的方法一般以object_开头
  • 对类进行操作的方法一般以class_开头
  • 对类或对象的方法进行操作的方法一般以method_开头
  • 对成员变量进行操作的方法一般以ivar_开头
  • 对属性进行操作的方法一般以property_开头开头
  • 对协议进行操作的方法一般以protocol_开头

例如:使用runtime对当前的应用中加载的类进行打印操作。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    unsigned int count = 0;
    Class *classes = objc_copyClassList(&count);
    for (int i = 0; i < count; i++) {
        const char *cname = class_getName(classes[i]);
        printf("%s\n", cname);
    }
}

Runtime应用

那么Runtime在我们实际开发中会起到说明作用呢?主要有以下几点:
1. 动态的添加对象的成员变量和方法,修改属性值和方法
2. 动态交换两个方法的实现
3. 实现分类也可以添加属性
4. 实现NSCoding的自动归档和解档
5. 实现字典转模型的自动转换
6. 动态创建一个类(比如KVO的底层实现)

OC的方法调用在Runtime

1.OC代码调用方法

Receiver *receiver = [[Receiver alloc] init];
 [receiver  message];

2.在编译时RunTime会将上述代码转化成[发送消息]

objc_msgSend(receiver,@selector(message));

下面我们通过一个简单的例子来讲解Runtime的常见应用。
创建Student类

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface Student : NSObject
@property(nonatomic,copy)NSString *name;
- (void)eat;
- (void)sleep;
@end

Student.m文件

#import "Student.h"
@implementation Student
- (void)eat{
    NSLog(@"%@吃饭了",self.name);
}
- (void)sleep{
    NSLog(@"%@睡觉了",self.name);
}
@end

1. 动态变量控制

- (void)changeVariable {
    Student *student = [Student new];
    student.name = @"库克";
    NSLog(@"%@",student.name);

    unsigned int count;
    Ivar *ivar = class_copyIvarList([student class], &count);
    for (int i = 0; i< count; i++) {
        Ivar var = ivar[i];
        const char *varName = ivar_getName(var);
        NSString *name = [NSString stringWithCString:varName encoding:NSUTF8StringEncoding];
        if ([name isEqualToString:@"_name"]) {
            object_setIvar(student, var, @"Steve Jobs");
            break;
        }
    }
    NSLog(@"%@",student.name);   
}

输出结果:

2017-05-22 11:06:00.153 Day2017-05-22[9296:1003059] 库克
2017-05-22 11:06:03.155 Day2017-05-22[9296:1003059] Steve Jobs

2.动态添加方法

void happyNewYear(id self, SEL _cmd){
    NSLog(@"你好库克");
}

注意:
1.void的前面没有+、-号,因为只是C的代码。
2.必须有两个指定参数(id self,SEL _cmd)

- (void)addMethod
{
    Student *student = [Student new];
    student.name = @"库克";

    class_addMethod([student class], @selector(join), (IMP)happyNewYear, "v@:");
    [student performSelector:@selector(join)];
}

输出结果:

2017-05-22 11:10:06.379 Day2017-05-22[9296:1003059] 你好库克

3. 动态为Category扩展加属性

XCode运行你在Category的.h文件申明@Property,编译通过,但运行时如果没有Runtime处理,进行赋值取值,就马上报错。
首先添加分类Student+Category
头文件:

#import "Student.h"
@interface Student (Category)
@property(nonatomic,copy)NSString *firstName;
@end

.m文件:

#import "Student+Category.h"
#import <objc/runtime.h>
@implementation Student (Category)
const char name;
- (void)setFirstName:(NSString *)firstName  {
    objc_setAssociatedObject(self, &name, firstName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)firstName {
   return  objc_getAssociatedObject(self, &name);
}
@end

调用:

- (void)addExtentionProperty
{
    Student *student = [Student new];
    student.firstName = @"Steve";
    NSLog(@"添加属性firstName结果:%@ ",student.firstName);
}

4.动态交换方法实现

- (void)exchangeMethod
{
    Student *student = [Student new];
    student.name = @"库克";
    [student eat];
    [student sleep];

    NSLog(@"----------交换方法实现-----------");
    Method m1 = class_getInstanceMethod([student class], @selector(eat));
    Method m2 = class_getInstanceMethod([student class], @selector(sleep));
    method_exchangeImplementations(m1, m2);

    [student eat];
    [student sleep];
}
作者:xiangzhihong8 发表于2017/6/12 22:03:47 原文链接
阅读:418 评论:0 查看评论

针对 CoordinatorLayout 及 Behavior 的一次细节较真

$
0
0

我认真不是为了输赢,我就是认真。– 罗永浩

我一直对 Material Design 很感兴趣,每次在官网上阅读它的相关文档时,我总会有更进一步的体会。当然,Material Design 并不是仅仅针对 Android 而言的,它其实是一套普遍性的设计规范。而对于 Android 开发人员而言,我们涉及的往往是它的实现。也就是一个个个性鲜明的类。比如 RecyclerView 、CardView、Palette 等等。并且为了让开发者更轻松地开发出符合 Material Design 设计规范的界面,Google 开发人员直接提供了一个兼容包,它就是 Android Support Design Library。

引用这个包需要在 build.gradle 中添加依赖。

compile 'com.android.support:design:25.0.1'

在这个包中,最核心的一个类就是 CoordinatorLayout。因为其它的类都需要与它进行相关联才能互动。而今天的主题就是讨论这个类的一些细节。
这里写图片描述

上图中这种高大上的视觉和交互效果,第一次看的时候我心头就痒痒的,恨不得立马就去实现它。然后,就百度查找相关的博文,但是风格我都不是很喜欢。我不喜欢文章中放一个 xml 布局文件,然后配置一些属性,然后就没有了。

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.frank.supportdesigndemo.ScrollingActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/app_bar_height"
        android:layout_marginTop="-28dp"
        android:fitsSystemWindows="true"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                android:src="@drawable/test"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.7"
                />
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/AppTheme.PopupOverlay" />

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_scrolling" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        app:layout_anchor="@id/app_bar"
        app:layout_anchorGravity="bottom|end"
        app:srcCompat="@android:drawable/ic_dialog_email" />

</android.support.design.widget.CoordinatorLayout>

我照着完成了,效果也达到了,但是感觉有些虚。我不得劲,或者说我内心纠结吧。内心千头万绪,上面的布局文件中,除了 Toolbar 我认识外,其它的控件,我一个都不熟悉。

我不喜欢这种感觉。因为我有许许多多的疑惑。

CoordinatorLayout 是什么?
CoordinatorLayout 有什么作用?
AppBarLayout 是什么?
AppBarLayout 有什么作用?
……

接下来的文章篇幅会比较长,大家仔细阅读就好。如果时间不够,可以直接拖动到文章最后总结的那一节。不明白的地方再到文章中间部分阅读相关内容就可以了。但我希望读者还是顺序方式阅读,因为我相信如果你有许多疑惑,我的学习过程也许可以给你一些提示或者启迪。

更多的真相

在编程领域,学习一个陌生的事物,最好的途径可能就是阅读它的官方文档或者是源代码。带着心中的困惑,我前往 Android 官网,直接挑最显眼的 CoordinatorLayout 来进行研究。所以这篇文章我主讲 CoordinatorLayout。
这里写图片描述

官网解释 CoordinatorLayout 是一个超级 FrameLayout,然后可以作为一个容器指定与 child 的一些交互规则。

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent {}

这是 CoordinatorLayout 的声明。它本质上就是一个 ViewGroup,注意的是它并没有继承自 FrameLayout,然后实现了 NestedScrollingParent 接口,我们先不管这个 NestedScrollingParent,NestedScrolliingParent 在文章后面适当的地方我会给出解释。

官网又说通过给CoordinaotrLayout 中的 child 指定 Behavior,就可以和 child 进行交互,或者是 child 之间互相进行相关的交互。并且自定义 View 时,可以通过 DefaultBehavior 这个注解来指定它关联的 Behavior。这里出现了新的名词:Behavior。于是,中断对 CoordinatorLayout 的跟踪,转到 Behavior 细节上来。
这里写图片描述
Behavior 其实是 CoordinatorLayout 中的一个静态内部类,并且是个泛型,接受任何 View 类型。官方文档真是惜字如金,更多的细节需要去阅读代码,也就是要靠猜测。这点很不爽的。好吧,官方文档说 Behavior 是针对 CoordinatorLayout 中 child 的交互插件。记住这个词:插件。插件也就代表如果一个 child 需要某种交互,它就需要加载对应的 Behavior,否则它就是不具备这种交互能力的。而 Behavior 本身是一个抽象类,它的实现类都是为了能够让用户作用在一个 View 上进行拖拽、滑动、快速滑动等手势。如果自己要定制某个交互动作,就需要自己实现一个 Behavior。

但是,对于我们而言,我们要实现一个 Behavior,我们用来干嘛呢?

是的,问问自己吧,我们如果自定义一个 Behavior,我们想干嘛?

前面内容有讲过,CoordinatorLayout 可以定义与它 child 的交互或者是某些 child 之间的交互。

我们先看看 Behavior 的代码细节,代码有精简。

public static abstract class Behavior<V extends View> {

    public Behavior() { }

    public Behavior(Context context, AttributeSet attrs) {}


    public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; }

    public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {  return false; }

    public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}


    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
            V child, View directTargetChild, View target, int nestedScrollAxes) {
        return false;
    }

    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
            View directTargetChild, View target, int nestedScrollAxes) {
        // Do nothing
    }

    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
        // Do nothing
    }

    public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
            int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        // Do nothing
    }

    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
            int dx, int dy, int[] consumed) {
        // Do nothing
    }

    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
            float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
            float velocityX, float velocityY) {
        return false;
    }
}

一般我们自定义一个 Behavior,目的有两个,一个是根据某些依赖的 View 的位置进行相应的操作。另外一个就是响应 CoordinatorLayout 中某些组件的滑动事件。
我们先看第一种情况。

两个 View 之间的依赖关系

如果一个 View 依赖于另外一个 View。那么它可能需要操作下面 3 个 API:

public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; }

public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {  return false; }

public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}

确定一个 View 对另外一个 View 是否依赖的时候,是通过 layoutDependsOn() 这个方法。注意参数,child 是要判断的主角,而 dependency 是宾角,如果 return true,表示依赖成立,反之不成立。当然,你可以复写这个方法对 dependency 进行类型判断否则是其它条件判断,然后再决定是否依赖。只有在 layoutDependsOn() 返回为 true 时,后面的 onDependentViewChanged() 和 onDependentViewRemoved() 才会被调用。

当依赖的那个 View 发生变化时,这个变化代码注释有解释,指的是 dependency 的尺寸和位置发生的变化,当有变化时 Behavior 的 onDependentViewChanged() 方法会被调用。如果复写这个方法时,改变了 child 的尺寸和位置参数,则需要返回 true,默认情况是返回 false。

onDependentView() 被调用时一般是指 dependency 被它的 parent 移除,或者是 child 设定了新的 anchor。

有了上面 3 个 API,我们就能应付在 CoordinatorLayout 中一个子 View 对别个一个子 View 的依赖情景了。

可能会有同学不明白,依赖是为何?或者说是何种依赖。为了避免概念过于空洞抽象。下面,我们用一个简单的例子来让大家感受一下,加深理解。

为了演示效果,我首先在屏幕上定义一个能够响应拖动的自定义 View,我叫它 DependencyView 好了。
这里写图片描述
它的代码很简单,主要是继承一个 TextView,然后在触摸事件中对自身位置进行位移。

public class DependencyView extends TextView {

    private final int mSlop;
    private float mLastX;
    private float mLastY;

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

    public DependencyView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public DependencyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        setClickable(true);

        mSlop = ViewConfiguration.getTouchSlop();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
       // return super.onTouchEvent(event);
        int action = event.getAction();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                mLastX = event.getX();
                mLastY = event.getY();
                break;

            case MotionEvent.ACTION_MOVE:
                int deltax = (int) (event.getX() - mLastX);
                int deltay = (int) (event.getY() - mLastY);
                if (Math.abs(deltax) > mSlop || Math.abs(deltay) > mSlop) {
                    ViewCompat.offsetTopAndBottom(this,deltay);
                    ViewCompat.offsetLeftAndRight(this,deltax);
                    mLastX = event.getX();
                    mLastY = event.getY();
                }

                break;

            case MotionEvent.ACTION_UP:
                mLastX = event.getX();
                mLastY = event.getY();
                break;

            default:
                break;

        }

        return true;
    }
}

前置条件已经确定好了,现在我们要向目标 Behavior 出发了。

做一个跟屁虫

实现一个 Behavior,让它支配一个 View 去紧紧跟随所依赖的 View。在这里,我们让依赖方始终显示在被依赖方的正下方,不论被依赖方位置怎么变换,依赖方始终紧紧相随。那么,代码怎么写。

public class MyBehavior extends CoordinatorLayout.Behavior <View>{

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof DependencyView;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        int dependBottom = dependency.getBottom();

        child.setY(dependBottom + 50);
        child.setX(dependency.getLeft());

        return true;
    }

}

我们省略了其它代码,只保留了核心的两个方法,大家一看就懂。通过判断 dependency 是否为 DependencyView 类型来决定是否对其进行依赖。然后在 onDependentViewChanged() 方法中获取 dependency 的位置参数来设置 child 的位置参数,从而实现了预期效果。注意的是更改 child 的位置后,要 return true。

下面来验证。我们在布局文件中对一个 ImageView 设置 MyBehavior,然后观察它的现象。

<ImageView
    android:id="@+id/iv_test"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@mipmap/ic_launcher"
    app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>

这里写图片描述

当然这种依赖,并非是一对一的关系,可能是一对多。或者是多对多。

我们再修改一个代码,如果 child 是一个 TextView 就让它始终在 dependency 的上方显示,否则在它下方显示。

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

    float x = child.getX();
    float y = child.getY();

    int dependTop= dependency.getTop();
    int dependBottom = dependency.getBottom();

    x = dependency.getX();

    if ( child instanceof TextView ) {
        y = dependTop - child.getHeight() - 20;
    } else {
        y = dependBottom + 50;
    }


    child.setX(x);
    child.setY(y);

    return true;
}

上面代码清晰易懂,我们再在 xml 布局文件中添加一个 TextView。

<ImageView
        android:id="@+id/iv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="for test"
    app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>

效果如下:
这里写图片描述

到此,我们算是弄明白了在 Behavior 中针对被依赖的对象尺寸及位置变化时,依赖方应该如何处理的流程了。接着往下的内容就是处理滑动相关了。不过,在这之前先对一个地方进行说明,那就是如何对于一个 View 设置 Behavior。

Behavior 的设置方法

1. 在 xml 属性中进行设置

对应属性是 app:layout_behavior。要设置的是一条字符串,一般是 Behavior 的全限定类名如 com.frank.supportdesigndemo.MyBehavior,当然,在当前目录下你可以用 . 代替如 .MyBehavior

2. 在代码中设置

主要是设置对应 View 的 LayoutParam

CoordinatorLayout.LayoutParams layoutParams = 
    (CoordinatorLayout.LayoutParams) mIvTest.getLayoutParams();

layoutParams.setBehavior(new MyBehavior());

3. 通过注解

自定义 View 时,通过 CoordinatorLayout.DefaultBehavior 这个注解,就可以为该 View 默认绑定一个对应的 Behavior。Android 源码中有现成的例子。

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {}

可以看到 AppBarLayout 被注解绑定了 AppBarLayout.Behavior 这个 Behavior。所以,之后要研究 AppBarLayout 的话也需要研究它的 Behavior。不过,这是后话。

Behavior 对滑动事件的响应。

其实对于这样的行为,我存在过困惑。官方文档的内容太少了,说的是滑动,但是我并不明白是什么滑动。是响应谁的滑动。

我们一般接触到的滑动控件是 ScrollView、ListView 和 RecyclerView。而 CoordinatorLayout 本身能够滑动吗?

滑动相关的代码如下:

public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
            V child, View directTargetChild, View target, int nestedScrollAxes) {
        return false;
    }

public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
        View directTargetChild, View target, int nestedScrollAxes) {
    // Do nothing
}

public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
    // Do nothing
}

public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    // Do nothing
}

public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dx, int dy, int[] consumed) {
    // Do nothing
}

public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY, boolean consumed) {
    return false;
}

public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY) {
    return false;
}

为了观察滑动这个行为,我在 MyBehavior 中进行编写了一些调试代码。

@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target,
                           int dxConsumed, int dyConsumed, int dxUnconsumed,
                           int dyUnconsumed) {
    Log.d(TAG,"onNestedScroll:"+dxConsumed+" dy:"+dyConsumed);
    super.onNestedScroll(coordinatorLayout, child, target, dxConsumed,
            dyConsumed, dxUnconsumed, dyUnconsumed);
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
                                   View directTargetChild, View target, int nestedScrollAxes) {
    Log.d(TAG,"onStartNestedScroll");
    return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
            nestedScrollAxes);
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    Log.d(TAG,"onNestedPreScroll  dx:"+dx+" dy:"+dy);
}

@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
                             float velocityX, float velocityY, boolean consumed) {
    Log.d(TAG,"onNestedFling");
    return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}

然后我就在模拟器上用鼠标在 CoordinatorLayout 上拼命滑动,想制造一些滑动事件出来,看看 MyBehavior 相应的 API 能不能触发,然后观察 Log。
这里写图片描述

很遗憾,我无功而返。

认真查阅文档和源码。我将注意力放在 onStartNestedScroll() 方法上了。

/**
 * Called when a descendant of the CoordinatorLayout attempts to initiate a nested scroll.
 *
 * <p>Any Behavior associated with any direct child of the CoordinatorLayout may respond
 * to this event and return true to indicate that the CoordinatorLayout should act as
 * a nested scrolling parent for this scroll. Only Behaviors that return true from
 * this method will receive subsequent nested scroll events.</p>
 *
 * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
 *                          associated with
 * @param child the child view of the CoordinatorLayout this Behavior is associated with
 * @param directTargetChild the child view of the CoordinatorLayout that either is or
 *                          contains the target of the nested scroll operation
 * @param target the descendant view of the CoordinatorLayout initiating the nested scroll
 * @param nestedScrollAxes the axes that this nested scroll applies to. See
 *                         {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
 *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL}
 * @return true if the Behavior wishes to accept this nested scroll
 *
 * @see NestedScrollingParent#onStartNestedScroll(View, View, int)
 */
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
        V child, View directTargetChild, View target, int nestedScrollAxes) {
    return false;
}

注释说了,当一个 CoordinatorLayout 的后代企图触发一个 nested scroll 事件时,这个方法被调用。nested scroll 我不知道是什么,有些人称呼为嵌套滑动。那就用嵌套滑动来翻译吧。注释中说过,只有在 onStartNestedSroll() 方法返回 true 时,后续的嵌套滑动事件才会响应。

后续的响应函数应该就是指的是这几个方法

public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
        View directTargetChild, View target, int nestedScrollAxes) {}

public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {}

public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {}

public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dx, int dy, int[] consumed) {}

public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY, boolean consumed) {}

public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY) {}

那么,我们从源头方法 onStartNestedScroll() 中开始分析。

借助于 AndroidStudio,我们很容易查找到 Behavior 中 onStartNestedScroll() 方法在哪里被调用。
这里写图片描述

原来,它是在 CoordinatorLayout 中被调用,我们跟进去看一看。

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    boolean handled = false;

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                    nestedScrollAxes);
            handled |= accepted;

            lp.acceptNestedScroll(accepted);
        } else {
            lp.acceptNestedScroll(false);
        }
    }
    return handled;
}

这是 CoordinatorLayout 中的一个方法,它获取子 View 的 Behavior,然后调用 Behavior 的 onStartNestedScroll() 方法。

再深入一点,谁调用了 CoordinatorLayout 的 onStartNestedScroll() 呢?

我们继续追踪。
这里写图片描述

发现有 3 个类可以调用它,一个是 View,另外一个是 ViewParentCompatLollipop 和 ViewParentCompatStubImpl。那么其实归根到底就是 View 和 ViewParentCompat。我们先从 View 开始分析好了。

public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = getParent();
        View child = this;
        while (p != null) {
            try {
                if (p.onStartNestedScroll(child, this, axes)) {
                    mNestedScrollingParent = p;
                    p.onNestedScrollAccepted(child, this, axes);
                    return true;
                }
            } catch (AbstractMethodError e) {
                Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " +
                        "method onStartNestedScroll", e);
                // Allow the search upward to continue
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

逻辑已经很清晰了,当一个 View 的 startNestedScroll() 方法触发时,如果符合规则,则会遍历自己的 parent,调用 parent 的 onStartNestedScroll() 方法。因为 CoordinatorLayout 是一个 ViewGroup,所以它就是一个 ViewParent 对象。所以,如果一个 CoordinatorLayout 中的后代触发了 startNestedScroll() 方法,如果符合某种条件,那么它的 onStartNestedScroll() 方法就会调用,再进一步会调用相应 Behavior 的方法。

注意我在上文中的措辞,我说过符合规则或者说符合某种条件,那么条件的具体是什么呢?

/**
 * Returns true if nested scrolling is enabled for this view.
 *
 * <p>If nested scrolling is enabled and this View class implementation supports it,
 * this view will act as a nested scrolling child view when applicable, forwarding data
 * about the scroll operation in progress to a compatible and cooperating nested scrolling
 * parent.</p>
 *
 * @return true if nested scrolling is enabled
 *
 * @see #setNestedScrollingEnabled(boolean)
 */
public boolean isNestedScrollingEnabled() {
    return (mPrivateFlags3 & PFLAG3_NESTED_SCROLLING_ENABLED) ==
            PFLAG3_NESTED_SCROLLING_ENABLED;
}

当 isNestedScrollingEnabled() 返回 true 时,它的 ViewParent 的 onStartNestedScroll() 才能被触发。这个方法的逻辑就是判断一个 View 中 mPrivateFlags3 这个变量中的 PFLAG3_NESTED_SCROLLING_ENABLED 这一 bit 是否被置为 1 。

注释有提到,另外一个方法 setNestedScrollingEnabled() 来设置能不能拥有嵌套滑动的能力。

public void setNestedScrollingEnabled(boolean enabled) {
    if (enabled) {
        mPrivateFlags3 |= PFLAG3_NESTED_SCROLLING_ENABLED;
    } else {
        stopNestedScroll();
        mPrivateFlags3 &= ~PFLAG3_NESTED_SCROLLING_ENABLED;
    }
}

看到这里的时候,我有了一个大胆的想法。

大胆,用 Button 产生一个 nested scroll 事件

如果一个 View 符合嵌套滑动的条件。也就是通过调用 setNestedScrollingEnabled(true),然后调用它的 startNestedScroll() 方法,它理论上是应该可以产生嵌套滑动事件的。好吧,我们来试一下,我们在布局文件中添加一个普通的 Button,然后给它设置点击事件。代码如下:

mBtnTest = (Button) findViewById(R.id.btn_nested_scroll);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    mBtnTest.setNestedScrollingEnabled(true);
}

mBtnTest.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mBtnTest.startNestedScroll(View.SCROLL_AXIS_HORIZONTAL);
        }
    }
});

因为我之前在 MyBehavior 中对相关的方法有 log 代码,所以如果 CoordinatorLayout 中发生嵌套滑动事件,log 是有输出的。

这里写图片描述

如上图所示,结果符合预期。不过,我们看上面的代码,当一个 View 只有在版本在 Lollipop 及以上时,它才能调用嵌套滑动相关的 api。如果是 5.0 版本以下呢?其实系统做了兼容。

mBtnTest = (Button) findViewById(R.id.btn_nested_scroll);
ViewCompat.setNestedScrollingEnabled(mBtnTest,true);
mBtnTest.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            ViewCompat.startNestedScroll(mBtnTest,View.SCROLL_AXIS_HORIZONTAL);
        }
    }
});

我们可以通过 ViewCompat 这个类来完成对应的操作。不停歇,继续跟踪下去。

public static void setNestedScrollingEnabled(View view, boolean enabled) {
        IMPL.setNestedScrollingEnabled(view, enabled);
}

通过 IMPL 这个代理来完成。

static final ViewCompatImpl IMPL;
static {
    final int version = android.os.Build.VERSION.SDK_INT;
    if (BuildCompat.isAtLeastN()) {
        IMPL = new Api24ViewCompatImpl();
    } else if (version >= 23) {
        IMPL = new MarshmallowViewCompatImpl();
    } else if (version >= 21) {
        IMPL = new LollipopViewCompatImpl();
    } else if (version >= 19) {
        IMPL = new KitKatViewCompatImpl();
    } else if (version >= 18) {
        IMPL = new JbMr2ViewCompatImpl();
    } else if (version >= 17) {
        IMPL = new JbMr1ViewCompatImpl();
    } else if (version >= 16) {
        IMPL = new JBViewCompatImpl();
    } else if (version >= 15) {
        IMPL = new ICSMr1ViewCompatImpl();
    } else if (version >= 14) {
        IMPL = new ICSViewCompatImpl();
    } else if (version >= 11) {
        IMPL = new HCViewCompatImpl();
    } else {
        IMPL = new BaseViewCompatImpl();
    }
}

针对不同的系统版本,IMPL 有不同的实现,所以它才能够做到兼容。我们知道在 Lollipop 版本 View 已经自带了嵌套滑动和相关属性和方法,现在我们就关心最低的版本,它们是如何处理这种情况的。最低的兼容版本是 BaseViewCompatImpl。

static class BaseViewCompatImpl implements ViewCompatImpl {

    @Override
    public void setNestedScrollingEnabled(View view, boolean enabled) {
        if (view instanceof NestedScrollingChild) {
            ((NestedScrollingChild) view).setNestedScrollingEnabled(enabled);
        }
    }

    @Override
    public boolean isNestedScrollingEnabled(View view) {
        if (view instanceof NestedScrollingChild) {
            return ((NestedScrollingChild) view).isNestedScrollingEnabled();
        }
        return false;
    }



    @Override
    public boolean startNestedScroll(View view, int axes) {
        if (view instanceof NestedScrollingChild) {
            return ((NestedScrollingChild) view).startNestedScroll(axes);
        }
        return false;
    }


}

代码有删简,但足够水落石出了。如果在 5.0 的系统版本以下,如果一个 View 想发起嵌套滑动事件,你得保证这个 View 实现了 NestedScrollingChild 接口。

想触发嵌套滑动事件吗?你是 NestedScrollingChild 吗?

如果在 5.0 的系统版本以上,我们要 setNestedScrollingEnabled(true),如果在这个版本以下,得保证这个 View 本身是 NestedScrollingChild 的实现类才行。现在就需要把焦点放在 NestedScrollingChild 上了。

public interface NestedScrollingChild {

    public void setNestedScrollingEnabled(boolean enabled);


    public boolean isNestedScrollingEnabled();


    public boolean startNestedScroll(int axes);


    public void stopNestedScroll();


    public boolean hasNestedScrollingParent();


    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);


    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);


    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);


    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

借助于 AndroidStudio,通过 CTRL + T 快捷键,我们可以得到目前 NestedScrollingChild 的实现者。

这里写图片描述

它有 4 个实现类:NavigationMenuView、NestedScrollView、RecyclerView、SwipleRefreshLayout。

好吧,这次追踪完结了。我们不需要将一个 View 设置能够滑动,然后再模拟滑动事件了。现在系统提供了 4 个来挑选。RecyclerView 和 SwipleRefreshLayout 我们自然是熟悉,NestedScrollView 看名字就可以联想到是能产生嵌套滑动的 ScrollView。

接下来的任务是什么?

别忘记了这一节的主题是自定义 Behavior。我们只在第一部分探索了 child 之间的依赖互动关系,还没有去讨论 Behavior 中如何响应嵌套滑动事件。之前的千回百转,我只是想找到能够挑起嵌套滑动事端的 View 而已。现在找到了之后,我们继续之前的话题。我们现在将一个 NestedScrollView 放进布局文件中,滑动它的内容,它将产生嵌套滑动事件。Behavior 需要针对自身业务逻辑进行相应的处理。

<android.support.v4.widget.NestedScrollView
        android:layout_marginTop="200dp"
        android:layout_width="300dp"
        android:layout_height="wrap_content">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/text_margin"
        android:text="@string/large_text" />
</android.support.v4.widget.NestedScrollView>

我们的目的是当 NestedScrollView 内容滑动时,MyBehavior 规定关联的 ImageView 对象进行相应的位移,这主要是在 Y 轴方向上。首先我们得实现这个方法。

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
                                   View directTargetChild, View target, int nestedScrollAxes) {
    Log.d(TAG,"onStartNestedScroll");
    return child instanceof ImageView && nestedScrollAxes == View.SCROLL_AXIS_VERTICAL;
}

只有 child 是 ImageView 类型,并且滑动的方向是 Y 轴时才响应。然后,我们可以针对滑动事件产生的位移对 child 进行操作了。

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    Log.d(TAG,"onNestedPreScroll  dx:"+dx+" dy:"+dy);
    ViewCompat.offsetTopAndBottom(child,dy);
}

我们要复写 onNestedPreScroll() 方法,dx 和 dy 是滑动的位移。另外还有一个方法 onNestedScroll()。两个方法的不同在于顺序的先后,onNestedPreScroll() 在 滑动事件准备作用的时候先行调用,注意是准备作用,然后把已经消耗过的距离传递给 consumed 这个数组当中。而 onNestedScroll() 是滑动事件作用时调用的。它的参数包括位移信息,以及已经在 onNestedPreScroll() 消耗过的位移数值。我们一般实现 onNestedPreScroll() 方法就好了。

在上面代码中,我们通过读取 dy 的值,来让 child 进行 Y 轴方向上的移动。
当然,在这之前我们要将 MyBehavior 做一些处理。将它与 TestView 解除依赖。

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    //return dependency instanceof DependencyView;
    return false;
}

然后,我们回顾下 xml 布局文件。

<ImageView
        android:id="@+id/iv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="for test"
    app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>

<android.support.v4.widget.NestedScrollView
    android:layout_marginTop="200dp"
    android:layout_width="300dp"
    android:layout_height="wrap_content">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/text_margin"
        android:text="@string/large_text" />
</android.support.v4.widget.NestedScrollView>

布局中 ImageView 和 TextView 同时被设置了 MyBehavior 属性,但是根据代码逻辑,最终应该只有 ImageView 能够进行 Y 轴方向的移动。那么事实如何?

这里写图片描述

效果达到了预期。

可能有细心的同学还会发现,Byhavior 中有两个与 Fling 相关的API。


public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY, boolean consumed) {
    return false;
}

public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY) {
    return false;
}

顾名思义,这就和 fling 操作有关。快速滑动 NestedScrollView 或者 RecyclerView,手指停下来的时候,滑动并没有马上停止,这就是 fling 操作。
与前面的 NestedScroll 相似,我们可以在 fling 动作即将发生时,通过 onNestedPreFling 获知,如果在这个方法返回值为 true 的话会怎么样?它将会拦截这次 fling 动作,表明响应中的 child 自己处理了这次 fling 意图,那么 NestedScrollView 反而操作不了这个动作,因为系统会当作 child 消耗过这次事件。大家可以自行去尝试一下。我们把注意点放在一个有趣的实验上。

这个实验的目的是当 MyBehavior 响应 fling 动作时,如果滑动方向向下,ImageView 就放大。反之缩小到原先的大小。

@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
    Log.d(TAG,"onNestedPreFling velocityY:"+velocityY);
    if ( velocityY > 0 ) {
        child.animate().scaleX(2.0f).scaleY(2.0f).setDuration(2000).start();
    } else {
        child.animate().scaleX(1.0f).scaleY(1.0f).setDuration(2000).start();
    }

    return false;
//        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}

在上面的代码中,我运用了一个 ViewPropertyAnimator 动画来对 scaleX 和 scaleY 进行处理。我们来看看效果吧。

这里写图片描述

挺有意思的是吗?

自定义 Behavior 的总结

  1. 确定 CoordinatorLayout 中 View 与 View 之间的依赖关系,通过 layoutDependsOn() 方法,返回值为 true 则依赖,否则不依赖。
  2. 当一个被依赖项 dependency 尺寸或者位置发生变化时,依赖方会通过 Byhavior 获取到,然后在 onDependentViewChanged 中处理。如果在这个方法中 child 尺寸或者位置发生了变化,则需要 return true。
  3. 当 Behavior 中的 View 准备响应嵌套滑动时,它不需要通过 layoutDependsOn() 来进行依赖绑定。只需要在 onStartNestedScroll() 方法中通过返回值告知 ViewParent,它是否对嵌套滑动感兴趣。返回值为 true 时,后续的滑动事件才能被响应。
  4. 嵌套滑动包括滑动(scroll) 和 快速滑动(fling) 两种情况。开发者根据实际情况运用就好了。
  5. Behavior 通过 3 种方式绑定:1. xml 布局文件。2. 代码设置 layoutparam。3. 自定义 View 的注解。

弄清楚上面的规则后,恭喜你,你已经掌握了自定义 Behavior 的基础技能。

再多一点细节?

不知道大家还记得不,文章前面部分我有试图去找出谁能产生 Nested Scroll 事件,结果发现它需要是 NestedScrollChild 对象。但是,我们忽略了一个细节,这个细节就是一个 NestedScrollChild 调用 startNestedScroll() 方法时,其实它需要借助它的祖先的力量。只有某个祖先的 onStartNestedScroll() 返回为真的时候,它持续事件才能延续下去。

view.java

public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = getParent();
        View child = this;
        while (p != null) {
            try {
                if (p.onStartNestedScroll(child, this, axes)) {
                    mNestedScrollingParent = p;
                    p.onNestedScrollAccepted(child, this, axes);
                    return true;
                }
            } catch (AbstractMethodError e) {
                Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " +
                        "method onStartNestedScroll", e);
                // Allow the search upward to continue
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

毫无疑问,ViewParent 充当了重要的角色。

public interface ViewParent {

    public void requestLayout();


    public void invalidateChild(View child, Rect r);


    public ViewParent getParent();


    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);


    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);


    public void onStopNestedScroll(View target);


    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);


    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);


    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);


    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

}

ViewParent 只是一个接口,常见的实现类是 ViewGroup,我曾经一度把 ViewParent 等同于 ViewGroup,结果在分析 View 绘制流程时吃尽苦头,因为 ViewParent 还有其它的实现类,比如 ViewRoot。不过这是题外话,我们接着聊 ViewParent。ViewParent 提供了 nested scroll 相关的 API,但是 5.0 版本才加进去的,如果要兼容的话,我们需要分析 ViewParentCompat 这个类。

public final class ViewParentCompat {

    interface ViewParentCompatImpl {
        public boolean requestSendAccessibilityEvent(
                ViewParent parent, View child, AccessibilityEvent event);
        boolean onStartNestedScroll(ViewParent parent, View child, View target,
                int nestedScrollAxes);
        void onNestedScrollAccepted(ViewParent parent, View child, View target,
                int nestedScrollAxes);
        void onStopNestedScroll(ViewParent parent, View target);
        void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed);
        void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, int[] consumed);
        boolean onNestedFling(ViewParent parent, View target, float velocityX, float velocityY,
                boolean consumed);
        boolean onNestedPreFling(ViewParent parent, View target, float velocityX, float velocityY);
        void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child,
                View source, int changeType);
    }

    static class ViewParentCompatStubImpl implements ViewParentCompatImpl {


        @Override
        public boolean onStartNestedScroll(ViewParent parent, View child, View target,
                int nestedScrollAxes) {
            if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
            return false;
        }
    }
}

这次,又挖掘出了一些真相。在 5.0 版本以下,如果一个 ViewParent 要响应嵌套滑动事件,就得保证它自己是一个 NestedScrollingParent 对象。

public interface NestedScrollingParent {


public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);


public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);


public void onStopNestedScroll(View target);


public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
        int dxUnconsumed, int dyUnconsumed);


public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);


public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);


public boolean onNestedPreFling(View target, float velocityX, float velocityY);


public int getNestedScrollAxes();
}

还是借助于 AndroidStudio 和快捷键 CTRL+T。我们可以得到它的实现类。
这里写图片描述

我们可以发现目前系统已经实现的 NestedScrollingParent 有 4 个:ActionBarOverlayLayout、CoordinatorLayout、NestedScrollView 和 SwipleRefreshLayout。

本文所讨论的 CoordinatorLayout 之所以能够处理嵌套滑动事件,这是因为它本身是一个 NestedScrollingParent。另外一个有意思的地方就是,NestedScrollView 它有两种身份,它同时实现了 NestedScrollingChild 和 NestedScrollParent 接口,也就说明它具备了两种能力,这为它本身的扩展提供了许多可能性。

Nested scroll 的流程

到这里的时候,一个嵌套滑动的事件的起始我们才彻底明白。它是由一个 NestedScrollingChild(5.0 版本 setNestedScrollEnable(true) 就好了) 发起,通过向上遍历 parent,借助于 parent 对象的相关方法来完成交互。值得注意的是 5.0 版本以下,parent 要保证是一个 NestedScrollingParent 对象。

我们今天的文章是分析 CoordinatorLayout 及它的 Behavior,所以用一张图来概念更清晰明了。
这里写图片描述

文章到此,又可以完结了。对于自定义一个 Behavior 而言,我们已经明白了它的功能及如何实现自己特定的功能。但是对于 CoordinatorLayout 本身而言,它还有许多细节需要说明。但是这些细节跟通用的自定义 ViewGroup 并无多大差别。唯一不同的地方的因为 Behavior 的存在。

CoordinatorLayout 的其它细节

Behavior 在之前也说过,它是一种插件。正因为这种机制,它将干涉 CoordinatorLayout 与 childView 之间的关系,Behavior 通过拦截 CoordinatorLayout 发给子 View 的信号,根据自身的规则进而来达到控制 childView 的目的。如果没有这些 Behavior 存在的话,CoordinatorLayout 跟普通的 ViewGroup 无疑。

那么,Behavior 干涉了 CoordinatorLayout 与它的 childView 之间的什么?可以说是方方面面。从测量、布局到触摸事件等等。
这里写图片描述

CoordinatorLayout 测量

CoordiantorLayout 是一个超级 FrameLayout,但是它却并不是 FrameLayout 的直接子类,只是一个普通的 ViewGroup 子类而已。FrameLayout 有什么物质?它就是一层一层的,按照位置信息进行布局,并没有像 LinearLayout 与 RelativeLayout 那么多约束。

所以,在查看相关代码之前,我们可以猜测的是,CoordinatorLayout 在 wrap_content 这种情况下,宽高的尺寸信息主要是要找出它子 View 的最大宽度或者最大高度,当然,还得参考 CoordianatorLayout 本身 parent 给它的建议尺寸。那么,实际情况是不是这样子呢?

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    prepareChildren();


    final int paddingLeft = getPaddingLeft();
    final int paddingTop = getPaddingTop();
    final int paddingRight = getPaddingRight();
    final int paddingBottom = getPaddingBottom();
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final boolean isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL;
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    final int widthPadding = paddingLeft + paddingRight;
    final int heightPadding = paddingTop + paddingBottom;

    int widthUsed = getSuggestedMinimumWidth();
    int heightUsed = getSuggestedMinimumHeight();
    int childState = 0;

    final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);

    final int childCount = mDependencySortedChildren.size();
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        int childWidthMeasureSpec = widthMeasureSpec;
        int childHeightMeasureSpec = heightMeasureSpec;


        final Behavior b = lp.getBehavior();
        if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                childHeightMeasureSpec, 0)) {
            onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0);
        }

        widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
                lp.leftMargin + lp.rightMargin);

        heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
                lp.topMargin + lp.bottomMargin);
        childState = ViewCompat.combineMeasuredStates(childState,
                ViewCompat.getMeasuredState(child));
    }

    final int width = ViewCompat.resolveSizeAndState(widthUsed, widthMeasureSpec,
            childState & ViewCompat.MEASURED_STATE_MASK);
    final int height = ViewCompat.resolveSizeAndState(heightUsed, heightMeasureSpec,
            childState << ViewCompat.MEASURED_HEIGHT_STATE_SHIFT);
    setMeasuredDimension(width, height);
}

上面的代码中,我精简了一些线索无关的代码。我们重点要关注 widthUsed 和 heightUsed 两个变量,它们的作用就是为了保存 CoordinatorLayout 中最大尺寸的子 View 的尺寸。并且,在对子 View 进行遍历的时候,CoordinatorLayout 有主动向子 View 的 Behavior 传递测量的要求,如果 Behavior 自主测量了 child,则以它的结果为准,否则将调用 measureChild() 方法亲自测量。

CoordinatorLayout 布局

在 FrameLayout 中布局默认从左上角开始,但是可以通过 layoutparam 中的 Gravity 进行布局对齐。那么,CoordinatorLayout 布局时会如何表现?

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final int childCount = mDependencySortedChildren.size();
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior behavior = lp.getBehavior();

        if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
            onLayoutChild(child, layoutDirection);
        }
    }
}

可以看到,CoordinatorLayout 将布局交给了子 View 的 Behavior,让它自行处理,如果 Behavior 没有处理关联的 View 布局的话,CoordinatorLayout 就会调用 onLayoutChild() 方法布局。我们再跟进去。

public void onLayoutChild(View child, int layoutDirection) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (lp.checkAnchorChanged()) {
        throw new IllegalStateException("An anchor may not be changed after CoordinatorLayout"
                + " measurement begins before layout is complete.");
    }
    if (lp.mAnchorView != null) {
        layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
    } else if (lp.keyline >= 0) {
        layoutChildWithKeyline(child, lp.keyline, layoutDirection);
    } else {
        layoutChild(child, layoutDirection);
    }
}

这一下引出了三个方法,并且有优先级的。layoutChildWithAnchor 优先级最高,然后是 layoutChildWithKeyline,最后才是普通的 layoutChild。

CoordinatorLayout 的锚定

LayoutParam 中有个 mAnchorView,Anchor 是锚点的意思,比如 View A 锚定了 View B,那么 View A 的 mAnchorView 就是 View B,布局的时候 View A 将参考 View B 的坐标。并且 layoutDirection 是参考的方向。它们都可以通过 xml 配置。

app:layout_anchor="@id/btn_coord"
app:layout_anchorGravity="bottom"

需要注意的是,当 View A 锚定 View B 时,就说明 View A 依赖于视图 View B。这个并不需要在 Behavior 中的 layoutDependsOn 返回 true。具体细节是因为下面的代码:

boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency == mAnchorDirectChild
            || shouldDodge(dependency, ViewCompat.getLayoutDirection(parent))
            || (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
}

上面这个方法是 LayoutPara 中的方法,判断 child 是不是依赖于 dependency,先判断的是 dependency 是否被 child 锚定,如果是的话就无需调用 Behavior 的 layoutDependsOn。

CoordinatorLayout 的参考线

除了锚定这个概念外,出现了 keyline 这个概念。keyline 应该是参考线的意思。指名这个 childView 布局时根据 keyline 的偏移量,再结合相应的 Gravity 进行布局。篇幅有限,感兴趣的同学自行去实践一下相应场景。

最后,我们聚集普通的 layoutChild() 方法。

private void layoutChild(View child, int layoutDirection) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    final Rect parent = mTempRect1;
    parent.set(getPaddingLeft() + lp.leftMargin,
            getPaddingTop() + lp.topMargin,
            getWidth() - getPaddingRight() - lp.rightMargin,
            getHeight() - getPaddingBottom() - lp.bottomMargin);

    if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this)
            && !ViewCompat.getFitsSystemWindows(child)) {
        // If we're set to handle insets but this child isn't, then it has been measured as
        // if there are no insets. We need to lay it out to match.
        parent.left += mLastInsets.getSystemWindowInsetLeft();
        parent.top += mLastInsets.getSystemWindowInsetTop();
        parent.right -= mLastInsets.getSystemWindowInsetRight();
        parent.bottom -= mLastInsets.getSystemWindowInsetBottom();
    }

    final Rect out = mTempRect2;
    GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
            child.getMeasuredHeight(), parent, out, layoutDirection);
    child.layout(out.left, out.top, out.right, out.bottom);
}

果真如我所推测的一样,它只是通过 GravityCompat.apply() 方法,通过 Gravity 确定 childView 在 parent 中的显示位置。这个效果就等同于了 FrameLayout。

通过测量、布局之后,CoordinatorLayout 就可以正常绘制了。但是如果要进行一些触摸输入间的交互就还要分析一个内容。这就是它的 touch 相关的事件。

@Override
public boolean onTouchEvent(MotionEvent ev) {
    boolean handled = false;
    boolean cancelSuper = false;
    MotionEvent cancelEvent = null;

    final int action = MotionEventCompat.getActionMasked(ev);

    if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
        // Safe since performIntercept guarantees that
        // mBehaviorTouchView != null if it returns true
        final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if (b != null) {
            handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
        }
    }

    // Keep the super implementation correct
    if (mBehaviorTouchView == null) {
        handled |= super.onTouchEvent(ev);
    } else if (cancelSuper) {
        if (cancelEvent == null) {
            final long now = SystemClock.uptimeMillis();
            cancelEvent = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
        }
        super.onTouchEvent(cancelEvent);
    }

    if (!handled && action == MotionEvent.ACTION_DOWN) {

    }

    if (cancelEvent != null) {
        cancelEvent.recycle();
    }

    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors();
    }

    return handled;
}

private boolean performIntercept(MotionEvent ev, final int type) {
        boolean intercepted = false;
        boolean newBlock = false;

        MotionEvent cancelEvent = null;

        final int action = MotionEventCompat.getActionMasked(ev);

        final List<View> topmostChildList = mTempList1;
        getTopSortedChildren(topmostChildList);

        // Let topmost child views inspect first
        final int childCount = topmostChildList.size();
        for (int i = 0; i < childCount; i++) {
            final View child = topmostChildList.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior b = lp.getBehavior();

            if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
                // Cancel all behaviors beneath the one that intercepted.
                // If the event is "down" then we don't have anything to cancel yet.
                if (b != null) {
                    if (cancelEvent == null) {
                        final long now = SystemClock.uptimeMillis();
                        cancelEvent = MotionEvent.obtain(now, now,
                                MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                    }
                    switch (type) {
                        case TYPE_ON_INTERCEPT:
                            b.onInterceptTouchEvent(this, child, cancelEvent);
                            break;
                        case TYPE_ON_TOUCH:
                            b.onTouchEvent(this, child, cancelEvent);
                            break;
                    }
                }
                continue;
            }

            if (!intercepted && b != null) {
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                        intercepted = b.onInterceptTouchEvent(this, child, ev);
                        break;
                    case TYPE_ON_TOUCH:
                        intercepted = b.onTouchEvent(this, child, ev);
                        break;
                }
                if (intercepted) {
                    mBehaviorTouchView = child;
                }
            }

            // Don't keep going if we're not allowing interaction below this.
            // Setting newBlock will make sure we cancel the rest of the behaviors.
            final boolean wasBlocking = lp.didBlockInteraction();
            final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
            newBlock = isBlocking && !wasBlocking;
            if (isBlocking && !newBlock) {
                // Stop here since we don't have anything more to cancel - we already did
                // when the behavior first started blocking things below this point.
                break;
            }
        }

        topmostChildList.clear();

        return intercepted;
    }

可以看到的是,如果 CoordinatorLayout 内部如果没有被拦截,那么它会传递触摸信息给 Behavior,如果有 Behavior 需要拦截这样的动作,那么就交给 Behavior,如果没有的话,它就会走传统的 ViewGroup 处理触摸事件的流程。

自此,CoordinatorLayout 绝大多数细节已经讨论完成。

总结

如果你是从头看到这里,我不知道你有没有这种感觉,像探索一样,经历了很长一段时间,顺着一条条线索,焦急、纠结,最终走出了一条道路。回首溯望,也许会有种风轻云淡的感觉。

这篇文章洋洋洒洒已经有千字以上了,因为篇幅过长,为了防止遗忘。现在可以将文章细节总结如下:

  1. CoordinatorLayout 是一个普通的 ViewGroup,它的布局特性类似于 FrameLayout。
  2. CoordinatorLayout 是超级 FrameLayout,它比 FrameLayout 更强悍的原因是它能与 Behavior 交互。
  3. CoordinatorLayout 与 Behavior 相辅相成,它们一起构建了一个美妙的交互系统。
  4. 自定义 Behavior 主要有 2 个目的:1 确定一个 View 依赖另外一个 View 的依赖关系。2 指定一个 View 响应嵌套滑动事件。
  5. 确定两个 View 的依赖关系,有两种途径。一个是在 Behavior 中的 layoutDepentOn() 返回 true。另外一种就是直接通过 xml 锚定一个 View。当被依赖方尺寸和位置变化时,Behavior 中的 onDependentViewChanged 方法会被调用。如果在这个方法中改变了主动依赖的那个 view 的尺寸或者位置信息,应该在方法最后 return true。
  6. 嵌套滑动分为 nested scroll 和 fling 两种。Behavior 中相应的 View 是否接受响应由 onStartNestedScroll() 返回值决定。一般在 onNestedPreScroll() 处理相应的 nested scroll 响应,在 onPreFling 处理 fling 事件。但是这个不绝对,根据实际情况决定。
  7. NestedScrollView 能够产生嵌套滑动事件是因为它本质上是一个 NestedScrollingChild 对象,而 CoordinatorLayout 能够响应是因为它本质上是一个 NestedScrollingParent 对象。
  8. Behavior 是一种插件机制,如果没有 Behavior 的存在,CoordinatorLayout 和普通的 FrameLayout 无异。Behavior 的存在,可以决定 CoordinatorLayout 中对应的 childview 的测量尺寸、布局位置、触摸响应。

最后,回到文章最开始的地方。我们已经熟悉了 CoordinatorLayout,NestedScrollView 也了解一点点。而对于 AppbarLayout 我们还不了解。但是之前的恐慌却不存在了,因为了解到 Behavior 的机制,我们可以知道 CoordinatorLayout 并不是一定要和 AppBarLayout 或者 FloatButton 一起配合使用,它是独立的,抛开它们,我们通过自定义 Behavior 也可以实现非常炫丽的交互效果。

而系统自定义的 Behavior 可以给开发者提供了许多场景的便利与降低开发难度。

不要重复造轮子。但不代表我们不需要去了解轮子。

接下来,我将会一一学习 Android Support Design 这个库中其它有意思的类,如 AppBarLayout、CollapsingToolbarLayout、FloatingActionButton 等等,当然,必不可少的是与它们配合使用的各类 Behavior,如非常牛逼的 AppBarLayout.ScrollingViewBehavior 和 BottomSheetBehavior 等等。

这个精彩世界值得我们探索。

源码github地址

作者:briblue 发表于2017/6/12 22:17:29 原文链接
阅读:187 评论:1 查看评论

Kotlin 从学习到 Android 第十章 扩展

$
0
0

与 C# 、Gosu 类似,Kotlin 也可以为一个类扩展一个新的功能,而不需要从类继承或使用任何类型的设计模式,如装饰( Decorator )。这是通过一种被称为扩展的特殊声明完成的。Kotlin 支持扩展函数和扩展属性。

扩展函数

声明扩展函数时,我们需要在函数名前加上一个接收类型,也就是被扩展的类型。例如:下面的代码为 MutableList 添加一个 swap 的新函数。

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] 
    this[index1] = this[index2]
    this[index2] = tmp
}

上面代码中的 this 代表的是 MutableList 。扩展了 swap 函数后,MutableList 类型的数据就可以直接使用 swap 函数了:

val l = mutableListOf(1, 2, 3)
l.swap(0, 2)

当然在扩展函数中我们也可以使用泛型:

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

对于不熟悉 Kotlin 的童鞋,上面的代码可能看不明白,下面我们为自己的类来扩展一个函数用以演示 Kotlin 的扩展功能:

fun main(args: Array<String>) {
    var ec = ExtensionedClass()
    ec.nameSetter("admin")
    ec.alertName()          // 这里可以直接调用扩展函数
    println(ec.name)        // hello admin , 可见扩展函数调用成功
}

// 为 ExtensionedClass 添加一个新的函数
fun ExtensionedClass.alertName(){
    this.name = "hello " + this.name
}

class ExtensionedClass{

    var name: String? = null

    fun nameSetter(str: String){
        name = str
    }
}

扩展解析静态

扩展实际上并不修改它们扩展的类。通过定义一个扩展,你不需要将新成员插入到一个类中,而仅仅是使用这个类型的 变量.扩展函数 来调用。

我们想要强调的是,扩展函数是静态分派的,也就是说,它们不是由接收者类型来实现的。这意味着被调用的扩展函数是由调用函数的表达式的类型决定的,而不是在运行时对表达式求值的结果。例如:

open class C

class D: C()

fun C.foo() = "c"

fun D.foo() = "d"

fun printFoo(c: C) {
    println(c.foo())
}

printFoo(D())           // c

打印结果为 c ,因为调用的扩展函数只依赖于已声明的参数 C 类型,即 C 类。

如果一个类有一个成员函数,并且一个扩展函数被定义为具有相同的接收者类型,相同的名称,并且适用于给定的参数,那么在调用时总会调用成员函数。例如:

class C {
    fun foo() { println("member") }
}

fun C.foo() { println("extension") }

如果我们调用 c.foo() ,将会打印 “member”, 而不是 “extension”。

但是,对于扩展函数来说,重载成员函数是完全可以的,这些函数具有相同的名称但是不同的签名:

class C {
    fun foo() { println("member") }
}

fun C.foo(i: Int) { println("extension") }

如果调用 C().foo(1) 将会打印 “extension”。

可 null 接收器

注意,可以用可空的接收方类型定义扩展。这样的扩展可以在对象变量上调用,即使它的值为 null ,并且可以在函数体中检查 this == null ,这将允许你在 Kotlin 中调用 toString() 函数而不用检查是否为 null : 因为在扩展函数中检测了。

fun Any?.toString(): String {
    if (this == null) return "null"
    // 检测完是否是 null 后,下面的 this 会自动转化为非 null 类型
    return toString()
}

扩展属性

扩展属性和扩展函数类似:

val <T> List<T>.lastIndex: Int
    get() = size - 1

注意,由于扩展实际上并没有将成员插入到类中,所以对于扩展属性来说没有有效的方法来拥有一个支持字段(backing field)。这就是为什么初始化器不允许扩展属性的原因。它们的行为只能通过显式地提供getter/setter来定义。例如:

val Foo.bar = 1 // 错误: 扩展属性不能被初始化

Companion 对象扩展

如果一个类有 companion 对象,那么你也可以为这个 companion 对象定义扩展函数和扩展属性:

class MyClass {
    companion object { }  // will be called "Companion"
}

fun MyClass.Companion.foo() {
    // ...
}

就像 companion 对象的常规成员一样,可以只使用类名作为限定符来调用它们:

MyClass.foo()

扩展的作用范围

大多数时候,我们在顶层定义扩展,也就是直接在包下面:

package foo.bar

fun Baz.goo() { ... } 

要在其声明包之外使用这种扩展,我们需要在调用点上导入它

package com.example.usage

import foo.bar.goo // 只引入 goo
                   // 或者
import foo.bar.*   // 全部引入

fun usage(baz: Baz) {
    baz.goo()
}

声明扩展成员

在一个类中,您可以为另一个类声明扩展。在这样的扩展中,有多个隐式接收器-对象成员可以在没有限定符的情况下被访问。扩展被声明的类的实例称为分派接收器,而扩展方法的接收者类型的实例称为扩展接收器。

class D {
    fun bar() { ... }
}

class C {
    fun baz() { ... }

    fun D.foo() {
        bar()   // calls D.bar
        baz()   // calls C.baz
    }

    fun caller(d: D) {
        d.foo()   // call the extension function
    }
}

如果在分派接收器和扩展接收器之间发生名称冲突,则扩展接收器优先。要引用调度接收器的成员,你可以使用限定语法

class C {
    fun D.foo() {
        toString()         // D.toString()
        this@C.toString()  // C.toString()
    }

声明为成员的扩展可以在子类中声明为 open 和 overridden 。这意味着分派这些函数对于分派接收器类型是虚拟的,但是对于扩展接收器类型来说是静态的。

open class D {
}

class D1 : D() {
}

open class C {
    open fun D.foo() {
        println("D.foo in C")
    }

    open fun D1.foo() {
        println("D1.foo in C")
    }

    fun caller(d: D) {
        d.foo()   // 调用扩展函数
    }
}

class C1 : C() {
    override fun D.foo() {
        println("D.foo in C1")
    }

    override fun D1.foo() {
        println("D1.foo in C1")
    }
}

C().caller(D())   // "D.foo in C"
C1().caller(D())  // "D.foo in C1" - dispatch receiver is resolved virtually
C().caller(D1())  // "D.foo in C" - extension receiver is resolved statically

使用扩展功能的动机

在 Java 中, 我们通常命名一些工具类,如:FileUtils, StringUtils 等等。著名的类 java.util.Collections 也是属于这种风格。但是当使用这些工具类时,会让我们感觉不舒服,例如:

// Java
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list))

当然,为了简化代码,我们可以通过静态导入达到下面的效果:

// Java
swap(list, binarySearch(list, max(otherList)), max(list))

这稍微好一点,但是 IDE 强大的代码补全功能对我们的帮助还是太少。如果能像下面这样就好了:

// Java
list.swap(list.binarySearch(otherList.max()), list.max())

但是,我们又不想在类中实现完所有有可能用到的方法,所以,这就是扩展功能的优势所在。

作者:niuzhucedenglu 发表于2017/6/12 23:03:04 原文链接
阅读:368 评论:0 查看评论

Tiny4412 Android5.0 定制media codecs相关的格式

$
0
0

tiny4412 4412 Android 5.0系统上,支持以下的media格式,文件位于: device/friendly-arm/tiny4412/media_codecs.xml

打开后我们可以看到这个xml包含相关的音视频编解码支持的格式:

<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright (C) 2012 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<!--
<!DOCTYPE MediaCodecs [
<!ELEMENT MediaCodecs (Decoders,Encoders)>
<!ELEMENT Decoders (MediaCodec*)>
<!ELEMENT Encoders (MediaCodec*)>
<!ELEMENT MediaCodec (Type*,Quirk*)>
<!ATTLIST MediaCodec name CDATA #REQUIRED>
<!ATTLIST MediaCodec type CDATA>
<!ELEMENT Type EMPTY>
<!ATTLIST Type name CDATA #REQUIRED>
<!ELEMENT Quirk EMPTY>
<!ATTLIST Quirk name CDATA #REQUIRED>
]>

There's a simple and a complex syntax to declare the availability of a
media codec:

A codec that properly follows the OpenMax spec and therefore doesn't have any
quirks and that only supports a single content type can be declared like so:

    <MediaCodec name="OMX.foo.bar" type="something/interesting" />

If a codec has quirks OR supports multiple content types, the following syntax
can be used:

    <MediaCodec name="OMX.foo.bar" >
        <Type name="something/interesting" />
        <Type name="something/else" />
        ...
        <Quirk name="requires-allocate-on-input-ports" />
        <Quirk name="requires-allocate-on-output-ports" />
        <Quirk name="output-buffers-are-unreadable" />
    </MediaCodec>

Only the three quirks included above are recognized at this point:

"requires-allocate-on-input-ports"
    must be advertised if the component does not properly support specification
    of input buffers using the OMX_UseBuffer(...) API but instead requires
    OMX_AllocateBuffer to be used.

"requires-allocate-on-output-ports"
    must be advertised if the component does not properly support specification
    of output buffers using the OMX_UseBuffer(...) API but instead requires
    OMX_AllocateBuffer to be used.

"output-buffers-are-unreadable"
    must be advertised if the emitted output buffers of a decoder component
    are not readable, i.e. use a custom format even though abusing one of
    the official OMX colorspace constants.
    Clients of such decoders will not be able to access the decoded data,
    naturally making the component much less useful. The only use for
    a component with this quirk is to render the output to the screen.
    Audio decoders MUST NOT advertise this quirk.
    Video decoders that advertise this quirk must be accompanied by a
    corresponding color space converter for thumbnail extraction,
    matching surfaceflinger support that can render the custom format to
    a texture and possibly other code, so just DON'T USE THIS QUIRK.

-->

<MediaCodecs>
    <Decoders>
        <MediaCodec name="OMX.SEC.MPEG4.Decoder" type="video/mp4v-es" />
        <MediaCodec name="OMX.SEC.H263.Decoder" type="video/3gpp" />
        <MediaCodec name="OMX.SEC.AVC.Decoder" type="video/avc" />
        <MediaCodec name="OMX.SEC.FP.AVC.Decoder" type="video/avc" />
        <MediaCodec name="OMX.SEC.MPEG2.Decoder" type="video/mpeg2" />
        <MediaCodec name="OMX.SEC.WMV.Decoder" type="video/vc1" />
        <MediaCodec name="OMX.SEC.MP3.Decoder" >
          <Type name="audio/mpeg" />
          <Quirk name="supports-multiple-frames-per-inputbuffer" />
          <Quirk name="needs-flush-before-disable" />
	    </MediaCodec>

        <MediaCodec name="OMX.google.mp3.decoder" type="audio/mpeg" />        
        <MediaCodec name="OMX.google.amrnb.decoder" type="audio/3gpp" />
        <MediaCodec name="OMX.google.amrwb.decoder" type="audio/amr-wb" />
        <MediaCodec name="OMX.google.aac.decoder" type="audio/mp4a-latm" />
        <MediaCodec name="OMX.google.g711.alaw.decoder" type="audio/g711-alaw" />
        <MediaCodec name="OMX.google.g711.mlaw.decoder" type="audio/g711-mlaw" />
        <MediaCodec name="OMX.google.vorbis.decoder" type="audio/vorbis" />

        <MediaCodec name="OMX.google.mpeg4.decoder" type="video/mp4v-es" />
        <MediaCodec name="OMX.google.h263.decoder" type="video/3gpp" />
        <MediaCodec name="OMX.google.h264.decoder" type="video/avc" />
        <MediaCodec name="OMX.google.vpx.decoder" type="video/x-vnd.on2.vp8" />
        <MediaCodec name="FfmpegVideoDecoder" type="video/ffmpeg" />
        <MediaCodec name="FfmpegAudioDecoder" type="audio/ffmpeg" />
    </Decoders>

    <Encoders>
        <MediaCodec name="OMX.SEC.MPEG4.Encoder" type="video/mp4v-es" />
        <MediaCodec name="OMX.SEC.H263.Encoder" type="video/3gpp" />
        <MediaCodec name="OMX.SEC.AVC.Encoder" type="video/avc" />

        <MediaCodec name="OMX.google.amrnb.encoder" type="audio/3gpp" />
        <MediaCodec name="OMX.google.amrwb.encoder" type="audio/amr-wb" />
        <MediaCodec name="OMX.google.aac.encoder" type="audio/mp4a-latm" />
        <MediaCodec name="OMX.google.flac.encoder" type="audio/flac" />
    </Encoders>
</MediaCodecs>
这里面,一部分是Android系统在发布的时候就自带支持的一些格式,而有一些格式是厂商加进去的,如果正式发布产品,有些格式还不是完全免费的,需要支付一定的认证费用才能使用对应的格式,那么再产品定制环节,我们可以根据需求来删除对应的格式,同时也可以添加对应的格式。

至于这部分,后面找到对应的java文件会再做分析:

(未完待补充!)



作者:morixinguan 发表于2017/6/12 23:41:41 原文链接
阅读:214 评论:0 查看评论

【我的Android进阶之旅】Android 混淆文件资源分类整理

$
0
0

之前将所有的混淆都配置在一个 proguard-rules.pro 这个Android Studio新建项目时自动生成的文件里面,而随着项目功能迭代越来越多,代码量越来越多,引用的第二方库、第三方库都越来越多,导致proguard-rules.pro 越来越臃肿,而且随着开发人员增多导致proguard-rules.pro 文件里面的配置越来越混乱。

一、拆分proguard-rules.pro混淆文件

因此今天我将proguard-rules.pro混淆文件进行拆分整理,大概拆分为以下4个文件:第三方混淆文件、第二份混淆文件、系统默认混淆文件、module单独混淆文件 。

这里写图片描述

如上图所示,四个文件分别为 'proguard-system-common.pro', 'proguard-module.pro', 'proguard-second-party.pro', 'proguard-third-party.pro'

二、配置混淆时候 proguardFiles 所引用的混淆文件

原来的混淆文件配置代码如下:

//混淆文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro',

改为下面的将四个混淆文件都加入到混淆配置文件列表中

 //混淆文件
 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-system-common.pro', 'proguard-module.pro', 'proguard-second-party.pro', 'proguard-third-party.pro'

如下图所示:

这里写图片描述

三、四个拆分的混淆文件具体代码

proguard-system-common.pro

proguard-system-common.pro 是Android系统通用的一些混淆配置,配置的内容大致如下:

# 指定代码的压缩级别
-optimizationpasses 5

-ignorewarnings # 抑制警告
-verbose    # 混淆时是否记录日志(混淆后生产映射文件 map 类名 -> 转化后类名的映射
-dontpreverify # 不预校验
-dontoptimize #不优化输入的类文件
-dontshrink #该选项 表示 不启用压缩  混淆时是否做预校验(可去掉加快混淆速度)

-dontusemixedcaseclassnames # 混淆时不会产生形形色色的类名  是否使用大小写混合
-dontskipnonpubliclibraryclasses #不跳过(混淆) jars中的 非public classes   默认选项

-keepattributes Exceptions # 解决AGPBI警告
-keepattributes Exceptions,InnerClasses,...
-keepattributes EnclosingMethod
-keepattributes SourceFile,LineNumberTable #运行抛出异常时保留代码行号
#过滤泛型
-keepattributes Signature
#过滤注解
-keepattributes *Annotation*
-keep class * extends java.lang.annotation.Annotation { *; }
-keep interface * extends java.lang.annotation.Annotation { *; }


#继承自activity,application,service,broadcastReceiver,contentprovider....不进行混淆
-keep public class * extends android.app.Fragment
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.support.v4.**
-keep public class com.android.vending.licensing.ILicensingService
-keep public class * extends android.support.multidex.MultiDexApplication
-keep public class * extends android.view.View
-keep class android.support.** {*;}

# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

# 所有View的子类及其子类的get、set方法都不进行混淆
-keep public class * extends android.view.View {  #保持自定义控件指定规则的方法不被混淆
    *** get*();
    void set*(***);
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

#保持指定规则的方法不被混淆(Android layout 布局文件中为控件配置的onClick方法不能混淆)
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

# 对于带有回调函数onXXEvent的,不能被混淆
-keepclassmembers class * {
    void *(*Event);
}

 #保持R文件不被混淆,否则,你的反射是获取不到资源id的
-keep class **.R$* { *; }
# 不混淆R类里及其所有内部static类中的所有static变量字段,$是用来分割内嵌类与其母体的标志
-keep public class **.R$*{
   public static final int *;
}
-keepclassmembers class **.R$* {
    public static <fields>;
}

#过滤js
-keepattributes *JavascriptInterface*
#保护WebView对HTML页面的API不被混淆
-keep class **.Webview2JsInterface { *; }
-keepclassmembers class * extends android.webkit.WebViewClient {  #如果你的项目中用到了webview的复杂操作 ,最好加入
     public void *(android.webkit.WebView,java.lang.String,android.graphics.Bitmap);
     public boolean *(android.webkit.WebView,java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebChromeClient {  #如果你的项目中用到了webview的复杂操作 ,最好加入
     public void *(android.webkit.WebView,java.lang.String);
}
#对WebView的简单说明下:经过实战检验,做腾讯QQ登录,如果引用他们提供的jar,若不加防止WebChromeClient混淆的代码,oauth认证无法回调,
#反编译基代码后可看到他们有用到WebChromeClient,加入此代码即可。

-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);  #保持自定义控件类不被混淆,指定格式的构造方法不去混淆
}

-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet, int);
}


# 保持枚举 enum 类不被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保持 native 方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保持 Parcelable 不被混淆
-keep class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator *;
}

#-keep public class * {
#    public protected *;
#}

#需要序列化和反序列化的类不能被混淆(注:Java反射用到的类也不能被混淆)
-keep public class * implements java.io.Serializable {
        public *;
}

#不混淆Serializable接口的子类中指定的某些成员变量和方法
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    !static !transient <fields>;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

-keepclassmembers class * {
   public <init> (org.json.JSONObject);
}

-keep class org.** {*;}
-keep class com.android.**{*;}
#-assumenosideeffects class_specification

#v7 不混淆
-keep class android.support.v7.** { *; }
-keep interface android.support.v7.** { *; }
-dontwarn android.support.v7.**

# support-design
-dontwarn android.support.design.**
-keep class android.support.design.** { *; }
-keep interface android.support.design.** { *; }
-keep public class android.support.design.R$* { *; }

# support-v7-appcompat
-dontwarn android.support.v7.**
-keep class android.support.v7.** { *; }
-keep class android.support.v7.internal.** { *; }
-keep interface android.support.v7.internal.** { *; }
-keep public class android.support.v7.widget.** { *; }
-keep public class android.support.v7.internal.widget.** { *; }
-keep public class android.support.v7.internal.view.menu.** { *; }

# support-v4
-dontwarn android.support.v4.**
-keep class android.support.v4.app.** { *; }
-keep interface android.support.v4.app.** { *; }
-keep class android.support.v4.** { *; }
-keep public class * extends android.support.v4.view.ActionProvider {
    public <init>(android.content.Context);
}

# support-v7-cardview.pro
# http://stackoverflow.com/questions/29679177/cardview-shadow-not-appearing-in-lollipop-after-obfuscate-with-proguard/29698051
-keep class android.support.v7.widget.RoundRectDrawable { *; }

-dontwarn android.net.http.**
-keep class org.apache.http.** { *;}

proguard-third-party.pro

proguard-third-party.pro 是通用的第三方AAR库的混淆配置

# >>>>>>>>>>>>>>>>>>>>>>>>  Facebook Fresco  Start >>>>>>>>>>>>>>>>>>>>>>>>
# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip

# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * {
    @com.facebook.common.internal.DoNotStrip *;
}

# Keep native methods
-keepclassmembers class * {
    native <methods>;
}

-dontwarn okio.**
-dontwarn javax.annotation.**
-dontwarn com.android.volley.toolbox.**

-dontwarn com.squareup.okhttp.**
-dontwarn okhttp3.**
# >>>>>>>>>>>>>>>>>>>>>>>>  Facebook Fresco  End >>>>>>>>>>>>>>>>>>>>>>>>



# >>>>>>>>>>>>>>>>>>>>>>>>  Glide  Start >>>>>>>>>>>>>>>>>>>>>>>>
# Glide specific rules #
# https://github.com/bumptech/glide
-dontwarn  com.bumptech.**
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
    **[] $VALUES;
    public *;
}
# >>>>>>>>>>>>>>>>>>>>>>>>  Glide  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>>  Picasso  Start >>>>>>>>>>>>>>>>>>>>>>>>
## Square Picasso specific rules ##
## https://square.github.io/picasso/ ##
-dontwarn com.squareup.okhttp.**
# >>>>>>>>>>>>>>>>>>>>>>>>  Picasso  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>>  Jackson  Start >>>>>>>>>>>>>>>>>>>>>>>>
-dontwarn com.fasterxml.jackson.databind.**
-keep class com.fasterxml.jackson.core.** {*;}
-keep interface com.fasterxml.jackson.core { *; }
-keep public class * extends com.fasterxml.jackson.core.**
-keep class com.fasterxml.jackson.databind.introspect.VisibilityChecker$Std.<clinit>
-keep class com.fasterxml.jackson.databind.ObjectMapper.<clinit>
-keep class com.fasterxml.jackson.databind.** {*;}
-keep class com.fasterxml.jackson.databind.introspect.VisibilityChecker$*{*;}
-keep interface com.fasterxml.jackson.databind { *; }
-keep public class * extends com.fasterxml.jackson.databind.**
# >>>>>>>>>>>>>>>>>>>>>>>>  Jackson  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>>  GSON  Start >>>>>>>>>>>>>>>>>>>>>>>>
## GSON 2.2.4 specific rules ##
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature

# For using GSON @Expose annotation
-keepattributes *Annotation*

-keepattributes EnclosingMethod

# Gson specific classes
-keep class sun.misc.Unsafe { *; }
-keep class com.google.gson.** { *; }
-keep class com.google.gson.stream.** { *; }
-keep class com.google.**{*;}
-keep class com.google.gson.examples.android.model.** { *; }
# >>>>>>>>>>>>>>>>>>>>>>>>  GSON  End >>>>>>>>>>>>>>>>>>>>>>>>

# >>>>>>>>>>>>>>>>>>>>>>>>  zxing  Start >>>>>>>>>>>>>>>>>>>>>>>>
-dontwarn com.google.zxing.**
-dontwarn com.google.zxing.client.android.**
-keep  class com.google.zxing.**{*;}
# >>>>>>>>>>>>>>>>>>>>>>>>  zxing  End >>>>>>>>>>>>>>>>>>>>>>>>



# >>>>>>>>>>>>>>>>>>>>>>>>  EventBus 2  Start >>>>>>>>>>>>>>>>>>>>>>>>
## GreenRobot EventBus specific rules ##
# https://github.com/greenrobot/EventBus/blob/master/HOWTO.md#proguard-configuration

-keepclassmembers class ** {
    public void onEvent*(***);
}

# Only required if you use AsyncExecutor
-keepclassmembers class * extends de.greenrobot.event.util.ThrowableFailureEvent {
    public <init>(java.lang.Throwable);
}

# Don't warn for missing support classes
-dontwarn de.greenrobot.event.util.*$Support
-dontwarn de.greenrobot.event.util.*$SupportManagerFragment
# >>>>>>>>>>>>>>>>>>>>>>>>  EventBus 2  End >>>>>>>>>>>>>>>>>>>>>>>>



# >>>>>>>>>>>>>>>>>>>>>>>>  EventBus 3  Start >>>>>>>>>>>>>>>>>>>>>>>>
## New rules for EventBus 3.0.x ##
# http://greenrobot.org/eventbus/documentation/proguard/

-keepattributes *Annotation*
-keepclassmembers class ** {
    @org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }

# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
    <init>(java.lang.Throwable);
}
# >>>>>>>>>>>>>>>>>>>>>>>>  EventBus 3  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>>  Rxjava  Start >>>>>>>>>>>>>>>>>>>>>>>>
-dontwarn sun.misc.**
-dontwarn org.apache.http.**

-keep class rx.**.

-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
   long producerIndex;
   long consumerIndex;
}

-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
    rx.internal.util.atomic.LinkedQueueNode producerNode;
}

-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
    rx.internal.util.atomic.LinkedQueueNode consumerNode;
}
# >>>>>>>>>>>>>>>>>>>>>>>>  Rxjava  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>>  OkHttp  Start >>>>>>>>>>>>>>>>>>>>>>>>
# OkHttp
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.squareup.okhttp.** { *; }
-keep interface com.squareup.okhttp.** { *; }
-dontwarn com.squareup.okhttp.**

# OkHttp3
-dontwarn okhttp3.**
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }

# Okio
-keep class sun.misc.Unsafe { *; }
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
-keep class okio.** {*;}
# >>>>>>>>>>>>>>>>>>>>>>>>  OkHttp  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>>  Retrofit2  Start >>>>>>>>>>>>>>>>>>>>>>>>
# Platform calls Class.forName on types which do not exist on Android to determine platform.
-dontnote retrofit2.Platform
# Platform used when running on RoboVM on iOS. Will not be used at runtime.
-dontnote retrofit2.Platform$IOS$MainThreadExecutor
# Platform used when running on Java 8 VMs. Will not be used at runtime.
-dontwarn retrofit2.Platform$Java8
# Retain generic type information for use by reflection by converters and adapters.
-keepattributes Signature
# Retain declared checked exceptions for use by a Proxy instance.
-keepattributes Exceptions

-dontwarn retrofit2.adapter.**

# Retrofit 2.X
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions

-keepclasseswithmembers class * {
    @retrofit2.http.* <methods>;
}

# >>>>>>>>>>>>>>>>>>>>>>>>  Retrofit2  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>>  Ormlite  Start >>>>>>>>>>>>>>>>>>>>>>>>
-keep class com.j256.ormlite.** {*;}
# >>>>>>>>>>>>>>>>>>>>>>>>  Ormlite  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>>  Butterknife  Start >>>>>>>>>>>>>>>>>>>>>>>>
# ButterKnife 7
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$$ViewBinder { *; }
-keep class **$$ViewInjector { *; }
-keepnames class * { @butterknife.InjectView *;}

-keepclasseswithmembernames class * {
    @butterknife.* <fields>;
}

-keepclasseswithmembernames class * {
    @butterknife.* <methods>;
}
# >>>>>>>>>>>>>>>>>>>>>>>>  Butterknife  End >>>>>>>>>>>>>>>>>>>>>>>>



# >>>>>>>>>>>>>>>>>>>>>>>>  热修复框架rocoofix  Start >>>>>>>>>>>>>>>>>>>>>>>>
-keep class com.dodola.rocoofix.** {*;}
-keep class com.lody.legend.** {*;}
# >>>>>>>>>>>>>>>>>>>>>>>>  热修复框架rocoofix  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>>  数据库加密不混淆  Start >>>>>>>>>>>>>>>>>>>>>>>>
-dontwarn com.google.common.**
-keep  class net.sqlcipher.** {*;}
-keep  class net.sqlcipher.database.** {*;}
# >>>>>>>>>>>>>>>>>>>>>>>>  数据库加密不混淆  End >>>>>>>>>>>>>>>>>>>>>>>>



# >>>>>>>>>>>>>>>>>>>>>>>> 七牛  Start >>>>>>>>>>>>>>>>>>>>>>>>
-keep class com.qiniu.** {*;}
-dontwarn com.qiniu.**
# >>>>>>>>>>>>>>>>>>>>>>>> 七牛  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>> UMeng大数据分析sdk混淆配置  Start >>>>>>>>>>>>>>>>>>>>>>>>
-keepclassmembers class * {
   public <init> (org.json.JSONObject);
}

-dontwarn org.apache.http.**

-keep class org.apache.http.** { *;}

-keep class com.umeng.analytics.** {*;}
-dontwarn com.umeng.analytics.**

# >>>>>>>>>>>>>>>>>>>>>>>> UMeng大数据分析sdk混淆配置  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>> 高德相关混淆文件  Start >>>>>>>>>>>>>>>>>>>>>>>>
-keep class com.amap.api.** {*;}
-keep class com.autonavi.** {*;}
-keep class com.a.a.** {*;}
-keep class com.loc.** {*;}
-dontwarn com.amap.api.**
-dontwarn com.autonavi.**
-dontwarn com.a.a.**
-dontwarn com.loc.**
# >>>>>>>>>>>>>>>>>>>>>>>> 高德相关混淆文件  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>> 百度定位  End >>>>>>>>>>>>>>>>>>>>>>>>
-keep class com.baidu.** {*;}
-keep class vi.com.** {*;}
-dontwarn com.baidu.**
# >>>>>>>>>>>>>>>>>>>>>>>> 百度定位  End >>>>>>>>>>>>>>>>>>>>>>>>


# >>>>>>>>>>>>>>>>>>>>>>>> pinyin4j  Start >>>>>>>>>>>>>>>>>>>>>>>>
-dontwarn net.soureceforge.pinyin4j.**
-dontwarn demo.**
-keep class net.sourceforge.pinyin4j.** { *;}
-keep class demo.** { *;}
-keep class com.hp.** { *;}
# >>>>>>>>>>>>>>>>>>>>>>>> pinyin4j  End >>>>>>>>>>>>>>>>>>>>>>>>

# >>>>>>>>>>>>>>>>>>>>>>>> httpclient  Start >>>>>>>>>>>>>>>>>>>>>>>>
-dontwarn android.net.compatibility.**
-dontwarn android.net.http.**
-dontwarn com.android.internal.http.multipart.**
-dontwarn org.apache.commons.**
-dontwarn org.apache.http.**
-dontwarn org.apache.http.protocol.**
-keep class android.net.compatibility.**{*;}
-keep class android.net.http.**{*;}
-keep class com.android.internal.http.multipart.**{*;}
-keep class org.apache.commons.**{*;}
-keep class org.apache.org.**{*;}
-keep class org.apache.harmony.**{*;}
# >>>>>>>>>>>>>>>>>>>>>>>> pinyin4j  End >>>>>>>>>>>>>>>>>>>>>>>>


-dontwarn net.sf.json.**
-keep class net.sf.json.** {*;}

# 动画库
-keep class com.nineoldandroids.** {*;}

proguard-second-party.pro

proguard-second-party.pro 是我们自己开发的一些存在Maven私服的AAR库的混淆配置

# zeteticSqlcipher
-keep  class net.sqlcipher.** {*;}
-keep  class net.sqlcipher.database.** {*;}

# watchSystem 
-keep class com.xtc.watch.** {*;}
-keep class com.xtc.watch.system.** {*;}

# data_log 
-keep class com.xtc.log.**{*;}
-keep class com.tencent.mars.**{*;}
-dontwarn com.tencent.mars.**

# bigData  
-dontwarn com.xtc.bigdata.**
-keep class com.xtc.bigdata.**{*;}
-keep class com.xtc.common.bigdata.**{*;}

# widget_watch | widget_watch 
-keep class com.xtc.widget.**{*;}
-dontwarn com.xtc.widget.**

# themeLib
-keep class com.xtc.themelib.**{*;}
-dontwarn com.xtc.themelib.**

# watchQiniu 
-dontwarn com.xtc.qiniu.**
-keep class com.xtc.qiniu.**{*;}

# data_watch 
-keep class com.xtc.data.**{*;}
-dontwarn com.xtc.data.**

# watchHttp 
-keep class com.xtc.httplib.**{*;}

# hotfix
-keep class com.xtc.hotfix.**{*;}

## sync_watch 
-keep class com.xtc.sync.bean.** {*;}

-keep class com.eebbk.filepreference.** {*;}
-keep class com.xtc.md5jni.** {*;}

proguard-module.pro

其中 proguard-module.pro 文件是你们自己模块的单独混淆文件,主要配置下你们项目中所引用到的一些网络和数据库的bean,防止json解析的时候出现解析出null的情况

#所有jackson对应实体类不能混淆
-keep class com.netease.xtc.cloudmusic.net.bean.** {*;}
#数据库实体类
-keep class com.xtc.data.watch.database.neteasemusic.bean.** {*;}

参考链接:
https://github.com/yongjhih/android-proguards


这里写图片描述

作者:欧阳鹏 欢迎转载,与人分享是进步的源泉!
转载请保留原文地址:http://blog.csdn.net/ouyang_peng/article/details/73088090

如果本文对您有所帮助,欢迎您扫码下图所示的支付宝和微信支付二维码对本文进行打赏。 

这里写图片描述

作者:qq446282412 发表于2017/6/12 10:26:07 原文链接
阅读:47 评论:0 查看评论

Android内存优化(二)DVM和ART的GC日志分析

$
0
0

相关文章
Android内存优化系列
Java虚拟机系列

前言

Java虚拟机(三)垃圾标记算法与Java对象的生命周期这篇文章中,提到了Java虚拟机的GC日志。DVM和ART的GC日志与Java虚拟机的日志有较大的区别,这篇文章就对DVM和ART的GC日志进行分析。

1.DVM的GC日志

在 DVM 中,每次垃圾收集都会将GC日志打印到 logcat 中,具体的格式为:

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>

可以看到DVM的日志共有5个信息,其中GC Reason有很多种,这里将它单独拿出来进行介绍。

引起GC原因

GC Reason就是指引起GC原因,有以下几种:

  • GC_CONCURRENT:当堆开始填充时,并发GC可以释放内存。
  • GC_FOR_MALLOC:当堆内存已满时,app尝试分配内存而引起的GC,系统必须停止app并回收内存。
  • GC_HPROF_DUMP_HEAP:当你请求创建 HPROF 文件来分析堆内存时出现的GC。
  • GC_EXPLICIT:显示的GC,例如调用System.gc()(应该避免调用显示的GC,信任GC会在需要时运行)。
  • GC_EXTERNAL_ALLOC:仅适用于 API 级别小于等于10 ,用于外部分配内存的GC。

其他信息

除了引起GC原因,其他的信息为:

  • Amount_freed:本次GC释放内存的大小。
  • Heap_stats:堆的空闲内存百分比 (已用内存)/(堆的总内存)。
  • External_memory_stats:API 级别 10 及更低级别的内存分配 (已分配的内存)/(引起GC的阀值)。
  • Pause time:暂停时间,更大的堆会有更长的暂停时间。并发暂停时间显示了两个暂停:一个出现在垃圾收集开始时,另一个出现在垃圾收集快要完成时。

实例分析

 D/dalvikvm: GC_CONCURRENT freed 2012K, 63% free 3213K/9291K, external 4501K/5161K, paused 2ms+2ms

这个GC日志的含义为:引起GC的原因是GC_CONCURRENT;本次GC释放的内存为2012K;堆的空闲内存百分比为63%,已用内存为3213K,堆的总内存为9291K;暂停的总时长为4ms。

2.ART的GC日志

ART的GC日志与DVM不同,ART 不会为没有明确请求的垃圾收集打印GC日志。只有在认为GC速度慢时才会打印GC日志,更确切来说,仅在GC暂停超过5ms 或GC持续时间超过 100ms 时才会打印GC日志。如果app未处于可察觉的暂停进程状态,那么它的GC不会被认为是慢速的。ART的GC日志始终会记录显式的垃圾收集。

ART的GC日志具体的格式为:

I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>

引起GC原因

ART的引起GC原因(GC_Reason)要比DVM多一些,有以下几种:

  • Concurrent: 并发GC,不会使App的线程暂停,该GC是在后台线程运行的,并不会阻止内存分配。
  • Alloc:当堆内存已满时,App尝试分配内存而引起的GC,这个GC会发生在正在分配内存的线程。
  • Explicit:App显示的请求垃圾收集,例如调用System.gc()。与DVM一样,最佳做法是应该信任GC并避免显示的请求GC,显示的请求GC会阻止分配线程并不必要的浪费 CPU 周期。如果显式的请求GC导致其他线程被抢占,那么有可能会导致 jank(App同一帧画了多次)。
  • NativeAlloc:Native内存分配时,比如为Bitmaps或者RenderScript分配对象, 这会导致Native内存压力,从而触发GC。
  • CollectorTransition:由堆转换引起的回收,这是运行时切换GC而引起的。收集器转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,收集器转换仅在以下情况下出现:在内存较小的设备上,App将进程状态从可察觉的暂停状态变更为可察觉的非暂停状态(反之亦然)。
  • HomogeneousSpaceCompact:齐性空间压缩是指空闲列表到压缩的空闲列表空间,通常发生在当App已经移动到可察觉的暂停进程状态。这样做的主要原因是减少了内存使用并对堆内存进行碎片整理。
  • DisableMovingGc:不是真正的触发GC原因,发生并发堆压缩时,由于使用了 GetPrimitiveArrayCritical,收集会被阻塞。一般情况下,强烈建议不要使用 GetPrimitiveArrayCritical,因为它在移动收集器方面具有限制。
  • HeapTrim:不是触发GC原因,但是请注意,收集会一直被阻塞,直到堆内存整理完毕。

垃圾收集器名称

GC_Name指的是垃圾收集器名称,有以下几种:

  • Concurrent mark sweep (CMS):CMS收集器是一种以获取最短收集暂停时间为目标收集器,采用了标记-清除算法(Mark-Sweep)实现。 它是完整的堆垃圾收集器,能释放除了Image Space之外的所有的空间。
  • Concurrent partial mark sweep:部分完整的堆垃圾收集器,能释放除了Image Space和Zygote Spaces之外的所有空间。关于Image Space和Zygote Spaces可以查看Android内存优化(一)DVM和ART原理初探这篇文章。
  • Concurrent sticky mark sweep:分代收集器,它只能释放自上次GC以来分配的对象。这个垃圾收集器比一个完整的或部分完整的垃圾收集器扫描的更频繁,因为它更快并且有更短的暂停时间。
  • Marksweep + semispace:非并发的GC,复制GC用于堆转换以及齐性空间压缩(堆碎片整理)。

其他信息

  • Objects freed:本次GC从非Large Object Space中回收的对象的数量。
  • Size_freed:本次GC从非Large Object Space中回收的字节数。
  • Large objects freed: 本次GC从Large Object Space中回收的对象的数量。
  • Large object size freed:本次GC从Large Object Space中回收的字节数。
  • Heap stats:堆的空闲内存百分比 (已用内存)/(堆的总内存)。
  • Pause times:暂停时间,暂停时间与在GC运行时修改的对象引用的数量成比例。目前,ART的CMS收集器仅有一次暂停,它出现GC的结尾附近。移动的垃圾收集器暂停时间会很长,会在大部分垃圾回收期间持续出现。

实例分析

I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms

这个GC日志的含义为:引起GC原因是Explicit ;垃圾收集器为CMS收集器;释放对象的数量为104710个,释放字节数为7MB;释放大对象的数量为21个,释放大对象字节数为416KB;堆的空闲内存百分比为33%,已用内存为25MB,堆的总内存为38MB;GC暂停时长为1.230ms,GC总时长为67.216ms。

参考资料
Investigating Your RAM Usage


欢迎关注我的微信公众号,第一时间获得博客更新提醒,以及更多成体系的Android相关原创技术干货。
扫一扫下方二维码或者长按识别二维码,即可关注。

作者:itachi85 发表于2017/6/13 0:10:00 原文链接
阅读:162 评论:0 查看评论

ReactNative开发——滑动组件

$
0
0

ReactNative开发——滑动组件

环境

window android react-native 0.45

ScrollView

介绍

ScrollView是一个可以滑动的组件,它内部可以是一个高度不受控制的View,但它自身必须要有个固定的高度。这里如果我们不给直接他设置高度,它的上层空间有固定高度的话也是可以的。

<ScrollView> VS <FlatList>我们应该选择哪个?

ScrollView 的工作是简单的渲染所有的子组件,它的用法比较简单。

FlatList 是惰性渲染item,只有当item出现在界面上时才渲染,并且从屏幕滑出去之后该item会被移除。所以它更省内存,也更节省cpu处理的时间。当然他的用法也比较复杂。

用法

我们来看一下用法示例,大家可以直接copy我的代码运行试试看:

/**
 * Created by blueberry on 6/9/2017.
 * @flow
 */

import React, {Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    View,
    Text,
    ScrollView,
    ListView,
    FlatList,
    RefreshControl,
    Dimensions,
    Button,
    TextInput,
} from 'react-native';

let totalWidth = Dimensions.get('window').width;
let totalHeight = Dimensions.get('window').height;

export default class MainPage extends Component {

    state = {
        isRefresh: false,
    }

    _onRefresh() {
        console.log('onRefresh.');

        this.setState({isRefresh: true});
        // 模拟获取数据需要三秒
        setTimeout(() => this.setState({isRefresh: false}), 3000);
    }

    _onScroll() {
        console.log('onScroll.');
    }

    render() {
        return (
            <View style={styles.container}>
                <Button title="滑动到底部" onPress={() => _outScrollView.scrollToEnd({animated: false})}/>
                <ScrollView
                    ref={(scrollView) => {
                        _outScrollView = scrollView;
                    }}
                    contentContainerStyle={styles.outScrollView}
                    onScroll={this._onScroll} //回调
                    scrollEventThrottle={100} // ios : 控制scroll回调的频率,没秒触发多少次
                    showsVerticalScrollIndicator={false} //设置不显示垂直的滚动条
                    keyboardDismissMode={'on-drag'} // 'none'默认值,滑动时不隐藏软件盘,
                    // ‘on-drag'滑动时隐藏软件盘.interactive :ios可用。上滑可以回复键盘
                    keyboardShouldPersistTaps={'always'} //'never'默认值,点击TextInput以外的组件,软键盘收起,
                    // 'always'不会收起,`handle` 当点击事件被子组件捕获时,
                    //键盘不会自动收起,但我用android测试了,发现没有效果
                    //我使用的版本:RectNative 0.45
                    refreshControl={
                        <RefreshControl refreshing={this.state.isRefresh}
                                        onRefresh={this._onRefresh.bind(this)}
                                        title={'load...'}
                                        tintColor={'#ff0000'}
                                        colors={['#ff0000', '#00ff00', '#0000ff']}
                                        progressBackgroundColor={'#ffff00'}
                        />
                    }
                >

                    <ScrollView horizontal={true} contentContainerStyle={styles.inScrollView}
                                showsHorizontalScrollIndicator={false} //不显示滑动条
                    >
                        <TextInput placeholder={'测试软键盘'}
                                   style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: '#fff1ae'}}/>
                        <View style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: 'blue'}}/>
                    </ScrollView>

                    <ScrollView horizontal={true} contentContainerStyle={styles.inScrollView}>
                        <TextInput placeholder={'测试软键盘'}
                                   style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: '#fff1ae'}}/>
                        <View style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: 'blue'}}/>
                    </ScrollView>

                    <ScrollView horizontal={true} contentContainerStyle={styles.inScrollView}>
                        <View style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: 'red'}}/>
                        <View style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: 'blue'}}/>
                    </ScrollView>


                    <ScrollView horizontal={false} contentContainerStyle={styles.inScrollView}>
                        <View style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: 'red'}}/>
                        <View style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: 'blue'}}/>
                    </ScrollView>
                </ScrollView>
            </View>
        );
    }
}


var
    styles = StyleSheet.create({
        container: {
            flex: 1,
            height: '50%',
            backgroundColor: 'grey',
        },

        outScrollView: {
            // flex: 1,  这里指定flex的话,会出现不能上下滑动,原因在这样会把 "内容高度定死了",所以最好不要设置高度/flex,让内容的高度自适应
            justifyContent: 'center',
            backgroundColor: 'green',
        },

        inScrollView: {
            padding: 20,
            backgroundColor: '#88ff73'
        }

    });
AppRegistry
    .registerComponent(
        'Project08'
        , () =>
            MainPage
    )
;

上面的代码创建了一个ScrollView其中嵌套了4个ScrollView,有3个是横向滑动,最后一个是纵向滑动。ps:这里竟然没有滑动冲突,我想说:“666”,这要是android原生开的话,这种布局可是比较麻烦的。

基本属性

属性 作用
contentContainerStyle 设置内层容器的样式。what?什么是内层容器?这里我的理解是,它这个ScroolView中还包装着一个View,这里View包含有我们设置的Item,大家想想,我们在android原生开发中使用呢ScrollView的时候,内层是不是一般也要嵌套一个LinearLayout用来存放子View吧
onScroll 回调方法,滑动的时候回调
scrollEventThrottle 这个之后ios有效,用来设置onScroll滑动的频率,可以节省性能,类型:number.表示1秒回调多少次
showsVerticalScrollIndicator 这个用来设置是否显示垂直滚动条,和他相似的还有showsHorizontalScrollIndicator
showsHorizontalScrollIndicator 用来设置是否显示横向滑动条
keyboardDismissMode 用来设置软件盘滑动的时候,是否隐藏的模式,none(默认值),拖拽时不隐藏软键盘 on-drag 当拖拽开始的时候隐藏软键盘 interactive 软键盘伴随拖拽操作同步地消失,并且如果往上滑动会恢复键盘。安卓设备上不支持这个选项,会表现的和none一样
keyboardShouldPersistTaps ‘never’(默认值),点击TextInput以外的子组件会使当前的软键盘收起。此时子元素不会收到点击事件。’always’,键盘不会自动收起,ScrollView也不会捕捉点击事件,但子组件可以捕获 ‘handled’,当点击事件被子组件捕获时,键盘不会自动收起。这样切换TextInput时键盘可以保持状态。多数带有TextInput的情况下你应该选择此项,但我用android机测试的时候,发现没卵用
refreshControl 用来设置下拉刷新组件,这个组件下文将介绍

ok,这些是基本属性,更多属性大家可以参考:http://reactnative.cn/docs/0.45/scrollview.html#content

RefreshControl

这个组件我上文的代码中,大家应该已经看到用法了。

refreshControl={
    <RefreshControl refreshing={this.state.isRefresh}
                    onRefresh={this._onRefresh.bind(this)}
                    title={'load...'}
                    tintColor={'#ff0000'}
                    colors={['#ff0000', '#00ff00', '#0000ff']}
                    progressBackgroundColor={'#ffff00'}
    />
}

需要的属性基本我都写上了,这里我再列个表格解释一下好了。

属性 作用
refreshing bool 类型,如果设置true,则下拉刷新按钮就一直显示着,如果设置false,就不显示,只有当下拉的时候显示
onRefresh 下拉回调
title 标题
tintColor 指定刷新指示器的颜色
colors 指定至少一种颜色用来绘制刷新指示器
progressBackgroundColor 指定刷新指示器的背景色

ListView

ListView是一个可以垂直滑动的组件,一般用来显示列表数据

用法

/**
 * Created by blueberry on 6/9/2017.
 * @flow
 */

import React, {Component} from 'react';
import {AppRegistry, StyleSheet, View, ListView, Text, Button} from 'react-native';
import StaticContainer from './StaticContainer';

let array = [];
{
    let len = 100;
    for (let i = 0; i < len; i++) {
        array.push('测试数据' + i);
    }
}

/**
 * 加个log,用来测试,是否更新。
 */
class LogView extends Component {
    componentDidUpdate() {
        console.log(this.props.name + 'Did update');
    }

    render() {
        return (
            <Text style={{backgroundColor: '#ffd98c'}}>
                我是:{this.props.name}
            </Text>
        );
    }
}

export default class ListViewPage extends Component {

    constructor() {
        super();
        let ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
        this.state = {
            //填充数据
            dataSource: ds.cloneWithRows(array),
        };
    }


    render() {
        return (
            <ListView
               // 数据源
                dataSource={this.state.dataSource}
                initialListSize={10} //初始的时候显示的数量
                onChangeVisibleRows={(visible, changedRows) => {
                     // 我用android测试,没有回调....
                    //    visible: 类型:{ sectionID: { rowID: true }}
                    //    { sectionID: { rowID: true | false }}
                    console.log('visible:' + JSON.stringify(visible));
                    console.log('changedRow:' + JSON.stringify(changedRows));
                }}
                onEndReached={() => console.log('onEndReached')}//当所有的数据都已经渲染过,并且列表被滚动到距离最底部不足
                // onEndReachedThreshold个像素的距离时调用。原生的滚动事件会被作为参数传递。译注:当第一次渲染时,
                // 如果数据不足一屏(比如初始值是空的),这个事件也会被触发,请自行做标记过滤。
                onEndReachedThreshold={2} //调用onEndReached之前的临界值,单位是像素
                pageSize={3} //每次渲染的行数
                // 返回一个头部可以渲染的组件
                renderHeader={() => (
                    <LogView name="header"/>
                )}

                //返回一个尾部可以渲染的组件
                renderFooter={() => (
                    <StaticContainer>
                        <LogView name="Footer"/>
                    </StaticContainer>
                )}

                //显示每一行
                renderRow={
                    (rowData, sectionID, rowID, highlightRow) => {
                        return (  <Text
                                style={{borderBottomColor: 'grey', borderBottomWidth: 1}}>
                                {'rowData:' + rowData + ' sectionId:' + sectionID + " rowId:" + rowID + " highlightRow:" + highlightRow}

                            </Text>
                        )
                    }}/>
        );
    }
}

AppRegistry.registerComponent('Project08', () => ListViewPage);

上面的代码实现了一个用来显示100条数据的列表,它还有一个头部,和一个尾部,因为头部和尾部的数据一般都不收布局变化,所有使用了一个StaticContainer来包装它,让他不刷新。这样做可以提高效率。为了看出效果,我特意定义了一个LogView组件,用来测试。

ListView.DataSource

ListView.DataSource 主要用来为ListView提供数据,它的一般用法。上面的代码已经给出了。

let ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
    //填充数据
    dataSource: ds.cloneWithRows(array),
};

它还有另外一个方法:cloneWithRowsAndSections(dataBlob, sectionIdentities, rowIdentities) 用来填充分组数据。
使用详细可以参考:http://reactnative.cn/docs/0.45/listviewdatasource.html#content

基本属性

属性名 作用
dataSource 数据源,上文已经说明
initialListSize 初始的时候显示的数量
onChangeVisibleRows 当可见的行的集合变化的时候调用此回调函数。visibleRows 以 { sectionID: { rowID: true }}的格式包含了所有可见行,而changedRows 以{ sectionID: { rowID: true
onEndReached 当所有的数据都已经渲染过,并且列表被滚动到距离最底部不足onEndReachedThreshold个像素的距离时调用。原生的滚动事件会被作为参数传递。译注:当第一次渲染时,如果数据不足一屏(比如初始值是空的),这个事件也会被触发,请自行做标记过滤。
onEndReachedThreshold 调用onEndReached之前的临界值,单位是像素。
pageSize 每次事件循环(每帧)渲染的行数。
renderFooter 页头与页脚会在每次渲染过程中都重新渲染(如果提供了这些属性)。如果它们重绘的性能开销很大,把他们包装到一个StaticContainer或者其它恰当的结构中。页脚会永远在列表的最底部,而页头会在最顶部。
renderHeader 和renderFoot的用法一样
renderRow (rowData, sectionID, rowID, highlightRow) => renderable 最重要的方法,用渲染每个item,其中rowData是你定义的数据列表中的类型数据,sectionID是该行的sectionID,rowId是该行的rowID,hightlighRow是一个函数引用,我目前没有发现卵用,官网说:如果item正在被高亮,可以通过hightlightRow(null) 来重置
scrollRenderAheadDistance 当一个行接近屏幕范围多少像素之内的时候,就开始渲染这一行
renderSectionHeader (sectionData, sectionID) => renderable 如果提供了此函数,会为每个小节(section)渲染一个粘性的标题。
stickySectionHeadersEnabled设置小节标题(section header)是否具有粘性
stickyHeaderIndices 一个子视图下标的数组,用于决定哪些成员会在滚动之后固定在屏幕顶端

额,还有ScrollView有的属性,ListView都有;所有horizontal、refreshControl,都可以个ListView设置。
基本属性就这些,更多属性参考:http://reactnative.cn/docs/0.45/listview.html#content

分组显示

/**
 * Created by blueberry on 6/9/2017.
 *
 * @flow
 */

import React, {Component} from 'react';
import {AppRegistry, StyleSheet, View, ListView, Text} from 'react-native';

let array: Array<Array<string>> = [];
{
    for (let i = 0; i < 10; i++) {
        let group: Array<string> = [];

        for (let j = 0; j < 10; j++) {
            group.push('分组:' + i + " item:" + j);
        }
        array['分组' + i] = group;

    }
}

export default class GroupListView extends Component {

    constructor() {
        super();

        var ds = new ListView.DataSource({
            rowHasChanged: (r1, r2) => r1 !== r2,
            sectionHeaderHasChanged: (pre, next) => pre !== next,
        });



        this.state = {
            /**
             * 填充数据
             * @params 所有的分组数据,结构{分组1:[分组1 item1,分组1item2. ...],分组2:[分组2item1,....]}
             * @param  sectionIdentities 每个分组的索引
             */
            dataSource: ds.cloneWithRowsAndSections(array,
                Object.keys(array)),

        };
    }

    render() {
        return (
            <ListView dataSource={this.state.dataSource} renderRow={(rowData) => {
                return <Text>{rowData}</Text>;
            }}
                      renderSectionHeader={(sectionData, sectionId) => {
                          console.log('sectionData:' + JSON.stringify(sectionData) + ' , sectionID:' + JSON.stringify(sectionId));
                          return <Text style={{backgroundColor: 'red'}}>{sectionData[0]}</Text>
                      }}
                      stickySectionHeadersEnabled={true} //开启之后,会有个粘性效果,
                    stickyHeaderIndices={[1]} //一个子视图下标的数组(这个下标连section也算在内的,),用于决定哪些成员会在滚动之后固定在屏幕顶端.根
                // stickySectionHeadersEnabled的效果很像
                      scrollRenderAheadDistance={10} //当一个行接近屏幕范围多少像素之内的时候,就开始渲染这一行。
            />
        );
    }
}

AppRegistry.registerComponent('Project08', () => GroupListView);

上面代码实现了用listView分组显示item。

FlatList

FlatList是ListView的升级版,它的性能比ListView好一些,但它目前刚出来,冒死还有些坑存在。。。

用法

/**
 * Created by blueberry on 6/11/2017.
 */

import React, {Component, PureComponent} from 'react';
import {AppRegistry, StyleSheet, View, FlatList, Text, TouchableOpacity} from 'react-native';

class ListItem extends PureComponent {
    _onPress = () => {
        this.props.onPressItem(this.props.id);
    }

    render() {
        let color = this.props.selected ? 'red' : 'blue';
        return (
            <TouchableOpacity
                style={{height: 200, justifyContent: 'center', flex: 1, alignItems: 'center', backgroundColor: color}
                } onPress={this._onPress}>
                <Text style={{fontSize: 20, height: 100,}}>{this.props.title}</Text>
                <Text style={{fontSize: 18, height: 80}}>{this.props.content}</Text>
            </TouchableOpacity>
        );
    }
}


/**
 * PureComponent 可以提高性能,只有在props或state发生改变时render。
 */
export default class FlatListPage extends PureComponent {

    state = {selected: (new Map(): Map<string, boolean>)};

    _onPressItem = (id: string) => {
        this.setState((state) => {
            const selected = new Map(state.selected);
            selected.set(id, !selected.get(id));//toggle.
            return {selected};
        });
    };

    _keyExtractor = (item, index) => item.id;

    /**
     *  使用箭头函数,既保证了this指向FlatListPage,也保证了不会每次都生成一个新的函数,这样在对比prop时,就返回'没有改变'
     */
    _renderItem = ({item}) => (
        <ListItem
            id={item.id}
            onPressItem={this._onPressItem}
            selected={!!this.state.selected.get(item.id)}
            title={item.title}
            content={item.content}
        />
    );

    render() {
        return (
            <FlatList
                data={this.props.data}
                renderItem={this._renderItem}
                // extraData={this.state}
                keyExtractor={this._keyExtractor}

                ItemSeparatorComponent={() => <View style={{height: 2, backgroundColor: 'black'}}/>} //分割线
                ListFooterComponent={() => <View style={{height: 50, backgroundColor: 'red'}}/>} //尾部布局
                ListHeaderComponent={() => <View style={{height: 50, backgroundColor: 'blue'}}/>} //头部布局
                 columnWrapperStyle={{height: 200, backgroundColor: 'green',}}
                numColumns={2} //
                //getItemCount={40}
                //getItemLayout={(data, index) => ({length: 200, offset: 200 * index, index})}
                refreshing={false}
                onEndReachedThreshold={20} //决定距离底部20个单位的时候,回到onEndReacted,但是我这只20,
                // 他距离4000左右的时候就回掉了,测试版本Android  reactNative Api:0.45
                onEndReached={(info) => {
                    console.log('onEndReacted:' + info.distanceFromEnd);
                }}
            />
        );
    }
}

{
    let array = [];
    for (let i = 0; i < 40; i++) {
        array[i] = {id: i, key: 'key' + i, title: '标题' + i, content: '内容' + i,};
    }
    FlatListPage.defaultProps = {data: array};
}


AppRegistry.registerComponent('Project08', () => FlatListPage);

常用属性

属性 作用
ItemSeparatorComponent 行与行之间的分隔线组件。不会出现在第一行之前和最后一行之后
ListFooterComponent 设置尾部组件
ListHeaderComponent 设置头部组件
columnWrapperStyle 如果设置了多列布局(即将numColumns值设为大于1的整数),则可以额外指定此样式作用在每行容器上。
data 为了简化起见,data属性目前只支持普通数组。如果需要使用其他特殊数据结构,例如immutable数组,请直接使用更底层的VirtualizedList组件。
extraData 如果有除data以外的数据用在列表中(不论是用在renderItem还是Header或者Footer中),请在此属性中指定。同时此数据在修改时也需要先修改其引用地址(比如先复制到一个新的Object或者数组中),然后再修改其值,否则界面很可能不会刷新。
keyExtractor此函数用于为给定的item生成一个不重复的key。Key的作用是使React能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。若不指定此函数,则默认抽取item.key作为key值。若item.key也不存在,则使用数组下标
numColumns 多列布局只能在非水平模式下使用,即必须是horizontal={false}。此时组件内元素会从左到右从上到下按Z字形排列,类似启用了flexWrap的布局。组件内元素必须是等高的——暂时还无法支持瀑布流布局

更多属性请参考: http://reactnative.cn/docs/0.45/flatlist.html#content

上述代码定义的组件都继承了 PureComponent,这个组件的作用是,只有prop和state它才render。实现它是为了提高效率。

SectionList

是一个高性能的分组列表组件

使用

/**
 * Created by blueberry on 6/12/2017.
 */

import React, {Component, PureComponent} from 'react';
import {AppRegistry, StyleSheet, View, SectionList, Text, RefreshControl} from 'react-native';

class ListItem extends PureComponent {

    render() {
        return (
            <Text style={{backgroundColor: 'red', height: 100}}>{this.props.title}</Text>
        );
    }

}

let sections = [];

for (let i = 0; i < 10; i++) {
    data = [];
    for (let j = 0; j < 10; j++) {
        data[j] = {title: '分组' + i + ',item' + j, id: j};
    }
    sections.push({data: data, key: '分组' + i});
    // 也可以使用下面方式自定义 不同section渲染不同类型的子组件
    //sections.push({data: data, key: '分组' + i, renderItem: i === 2 ? ()=><ListItem title="测试"/> : undefined});
}


export default class SectionListPage extends PureComponent {

    //为每一行生成唯一的key
    _keyExtractor = (item, index) => '' + item.key + index;


    render() {
        console.log(JSON.stringify(sections));
        return (
            <SectionList

                //渲染item的组件
                renderItem={({item}) =>
                    <ListItem
                        title={item.title}
                    />
                }
                //渲染sectionHeader的组件
                renderSectionHeader={({section}) =>
                    <Text>{section.key}</Text>
                }
                //数据
                sections={sections}
                //生成唯一的key
                keyExtractor={this._keyExtractor}


                ItemSeparatorComponent={() => <View style={{height: 2, backgroundColor: 'black'}}/>} //分割线
                ListFooterComponent={() => <View style={{height: 50, backgroundColor: 'red'}}/>} //尾部布局
                ListHeaderComponent={() => <View style={{height: 50, backgroundColor: 'blue'}}/>} //头部布局
            />
        );
    }
}

AppRegistry.registerComponent('Project08', () => SectionListPage);

基本属性

属性 作用
renderItem 和FlatList的renderItem作用一样。用来设置渲染item的组件,但SectionList,也可以section数据源中设置renderItem这个属性
renderSectionHeader 设置渲染分组小标签的组件
seciton 设置数据源

其余属性和FlatList中的属性作用一样,这里就不在介绍了。

我在上述代码中有这么一行,用来设置不同的renderItem函数,读者可以去掉注释看看效果
// sections.push({data: data, key: '分组' + i, renderItem: i === 2 ? ()=><ListItem title="测试"/> : undefined});

总结

其实我们文章中我们主要提到了 6个组件:ScrollView ListView RefreshControl FlatList SectionList PureComponent;其中主要讲解了四个滑动组件
- ScrollView
它没有懒加载功能,适合少量数据显示,用法比较简单。
- ListView
它用来显示列表数据,是懒加载Item,支持下拉刷新,上拉加载应该用它的onEndReached也是可以办到的。也支持分组列表显示,分组标签粘性滑动等功能,性能比较好。设置数据需要结合ListView.DataSource组件。
- FlatList
可以说是ListView的升级版,性能比ListView要好,同样支持下拉刷新等功能,目前我用0.45版本,刚出来,官方说还不稳定。
- SectionList
用来显示分组列表组件,性能比较高。

上面就是本文介绍的四个滑动组件,他们都支持下拉刷新组件,(ScrollView)有的属性,其他滑动组件基本都有。
RefreshControl就是官方给出的下拉刷新组件,用来设置到滑动组件上。
PureComponent之后当state或props变了之后,才能刷新。可以提高性能。

ok,介绍到这里了,其中我写的测试源码,在上文都贴出来了,大家可以测试测试。

参考

ReactNative 官网:http://facebook.github.io/react-native/releases/0.43/docs/flatlist.html
ReactNative 中文网:http://reactnative.cn/docs/0.45/sectionlist.html#content

作者:a992036795 发表于2017/6/13 9:27:14 原文链接
阅读:101 评论:0 查看评论

H.264标准(一)mp4封装格式详解

$
0
0

MP4(MPEG-4 Part 14)是一种常见的多媒体容器格式,它是在“ISO/IEC 14496-14”标准文件中定义的,属于MPEG-4的一部分,是“ISO/IEC 14496-12(MPEG-4 Part 12 ISO base media file format)”标准中所定义的媒体格式的一种实现(在H.264标准文档约14章位置),后者定义了一种通用的媒体文件结构标准。MP4是一种描述较为全面的容器格式,被认为可以在其中嵌入任何形式的数据,各种编码的视频、音频等都不在话下,不过我们常见的大部分的MP4文件存放的AVC(H.264)或MPEG-4(Part 2)编码的视频和AAC编码的音频。MP4格式的官方文件后缀名是“.mp4”,还有其他的以mp4为基础进行的扩展或者是阉割版的格式,如:M4V, 3GP, F4V等。

mp4是由一个个“box”组成的,大box中存放小box,一级嵌套一级来存放媒体信息。box的基本结构如下:
  
这里写图片描述

File Type Box(ftyp):
该box有且只有1个,并且只能被包含在文件层,而不能被其他box包含。该box应该被放在文件的最开始,指示该MP4文件应用的相关信息。

“ftyp” body依次包括1个32位的major brand(4个字符),1个32位的minor version(整数)和1个以32位(4个字符)为单位元素的数组compatible brands。

这里写图片描述

Movie Box(moov):

  • 该box包含了文件媒体的metadata信息,“moov”是一个container box,具体内容信息由子box诠释。同File Type Box一样,该box有且只有一个,且只被包含在文件层。一般情况下,“moov”会紧随“ftyp”出现。
  • 一般情况下,“moov”中会包含1个“mvhd”和若干个“trak”。其中“mvhd”为header box,一般作为“moov”的第一个子box出现(对于其他container box来说,header box都应作为首个子box出现)。“trak”包含了一个track的相关信息,是一个container box。

这里写图片描述

“mvhd”结构如下:

这里写图片描述

“trak”也是一个container box,其子box包含了该track的媒体数据引用和描述(hint track除外)。一个MP4文件中的媒体可以包含多个track,且至少有一个track,这些track之间彼此独立,有自己的时间和空间信息。“trak”必须包含一个“tkhd”和一个“mdia”,此外还有很多可选的box(略)。其中“tkhd”为track header box,“mdia”为media box,该box是一个包含一些track媒体数据信息box的container box。

这里写图片描述

“tkhd”结构如下表:

这里写图片描述

Handler Reference Box(hdlr)

“hdlr”解释了媒体的播放过程信息,该box也可以被包含在meta box(meta)中。“hdlr”结构如下表。

这里写图片描述

stbl

“stbl”包含了关于track中sample所有时间和位置的信息,以及sample的编解码等信息。利用这个表,可以解释sample的时序、类型、大小以及在各自存储容器中的位置。“stbl”是一个container box,其子box包括:sample description box(stsd)、time to sample box(stts)、sample size box(stsz或stz2)、sample to chunk box(stsc)、chunk offset box(stco或co64)、composition time to sample box(ctts)、sync sample box(stss)等。

“stsd”必不可少,且至少包含一个条目,该box包含了data reference box进行sample数据检索的信息。没有“stsd”就无法计算media sample的存储位置。“stsd”包含了编码的信息,其存储的信息随媒体类型不同而不同。

Time To Sample Box(stts)

“stts”存储了sample的duration,描述了sample时序的映射方法,我们通过它可以找到任何时间的sample。“stts”可以包含一个压缩的表来映射时间和sample序号,用其他的表来提供每个sample的长度和指针。表中每个条目提供了在同一个时间偏移量里面连续的sample序号,以及samples的偏移量。递增这些偏移量,就可以建立一个完整的time to sample表。

这里写图片描述

Sync Sample Box(stss)

“stss”确定media中的关键帧。对于压缩媒体数据,关键帧是一系列压缩序列的开始帧,其解压缩时不依赖以前的帧,而后续帧的解压缩将依赖于这个关键帧。“stss”可以非常紧凑的标记媒体内的随机存取点,它包含一个sample序号表,表内的每一项严格按照sample的序号排列,说明了媒体中的哪一个sample是关键帧。如果此表不存在,说明每一个sample都是一个关键帧,是一个随机存取点。

这里写图片描述

Sample To Chunk Box(stsc)

用chunk组织sample可以方便优化数据获取,一个thunk包含一个或多个sample。“stsc”中用一个表描述了sample与chunk的映射关系,查看这张表就可以找到包含指定sample的thunk,从而找到这个sample。

这里写图片描述

Sample Size Box(stsz)

“stsz” 定义了每个sample的大小,包含了媒体中全部sample的数目和一张给出每个sample大小的表。这个box相对来说体积是比较大的。

这里写图片描述

Chunk Offset Box(stco)

“stco”定义了每个thunk在媒体流中的位置。位置有两种可能,32位的和64位的,后者对非常大的电影很有用。在一个表中只会有一种可能,这个位置是在整个文件中的,而不是在任何box中的,这样做就可以直接在文件中找到媒体数据,而不用解释box。需要注意的是一旦前面的box有了任何改变,这张表都要重新建立,因为位置信息已经改变了。

这里写图片描述

Sample Description Box(stsd)

box header和version字段后会有一个entry count字段,根据entry的个数,每个entry会有type信息,如“vide”、“sund”等,根据type不同sample description会提供不同的信息,例如对于video track,会有“VisualSampleEntry”类型信息,对于audio track会有“AudioSampleEntry”类型信息。

视频的编码类型、宽高、长度,音频的声道、采样等信息都会出现在这个box中。

最后全家福:

这里写图片描述

这里写图片描述

这里写图片描述

作者:hejjunlin 发表于2017/6/13 10:28:44 原文链接
阅读:19 评论:0 查看评论

MVVM与Controller瘦身实践

$
0
0

前言

MVC是一个做iOS开发都知道的设计模式,也是Apple官方推荐的设计模式。实际上,Cocoa Touch就是按照MVC来设计的。

这里,我们先不讲MVC是什么,我们先来谈谈软件设计的一些原则或者说理念。在开发App的时候,我们的基本目标有以下几点:

  • 可靠性 - App的功能能够正常使用
  • 健壮性 - 在用户非正常使用的时候,app也能够正常反应,不要崩溃
  • 效率性 - 启动时间,耗电,流量,界面反应速度在用户容忍的范围以内

上文三点是表象层的东西,是大多数开发者或者团队会着重注意的。除了这三点,还有一些目标是工程方面的也是开发者要注意的:

  • 可修改性/可扩展性 - 软件需要迭代,功能不断完善
  • 容易理解 - 代码能够容易理解
  • 可测试性 - 代码能够方便的编写单元测试和集成测试
  • 可复用性 - 不用一次又一次造轮子

于是,软件设计领域有了几大通用设计原则来帮助我们实现这些目标:

单一功能原则,最少知识原则,聚合复用原则,接口隔离原则,依赖倒置原则,里氏代换原则,开-闭原则

这里的每一个原则都可以写单独的一篇文章,本文篇幅有限,不多讲解。

基于这些设计目标和理念,软件设计领域又有了设计模式。MVC/MVVM都是就是设计模式的一种。


MVC

历史

二十世纪世纪八十年代,Trygve Reenskaug在访问Palo Alto(施乐帕克)实验室的时候,第一次提出了MVC,并且在Smalltalk-76进行了实践,大名鼎鼎的施乐帕克实验室有很多划时代的研发成果:个人电脑,以太网,图形用户界面等。

在接下来的一段时间内,MVC不断的进化,基于MVC又提出了诸如MVP(model–view–presenter),MVVM(model–view–viewmodel)等设计模式。


组件

MVC设计模式按照职责将应用中的对象分成了三部分:Model,View,Controller。MVC除了将应用划分成了三个模块,还定义了模块之间的通信方式

Model

Model定义了你的应用是什么(What)。Model通常是纯粹的NSObject子类(Swift中可以是Struct/Class),仅仅用来表示数据模型。

Controller

Controller定义了Model如何显示给用户(How),并且View接收到的事件反馈到最后Model的变化。Controller层作为MVC的枢纽,往往要承担许多Model与View同步的工作。

View

View是Model的最终呈现,也就是用户看到的界面。


优点

MVC设计模式是是一个成熟的设计模式,也是Apple推荐的的设计模式,即使是刚入行的iOS开发者也多少了解这个设计模式,所以对于新人来说上手迅速,并且有大量的文档和范例来供我们参考。

在MVC模式中,View层是比较容易复用的,对应Cocoa中的UIView及其子类。所以,github的iOS开源项目中,View层也是最多的。

Model层涉及到了应用是什么,这一层非常独立,但是往往和具体业务相关,所以很难跨App服用。

既然只有Model-View-Controller三个组件,那么剩余的逻辑层代码就比较清楚了,全部堆积到Controller。


通信

MVC不仅定义了三类组件,还定义了组件之间通信的方式。

MVC三个组件之间的通信方式如图

Controller作为枢纽,它指向view和Model的线都是绿色的,意味着Controller可以直接访问(以引用的方式持有)Model和View。

View指向Controller的是虚线,虚线表示View到Controller的通信是盲通信的,原因也很简单:View是纯粹的展示部分,它不应该知道Controller是什么,它的工作就是拿到数据渲染出来。

那么,何为盲通信呢?简单来说当消息的发送者不知道接受者详细信息的时候,这样的通信就是盲通信。Cocoa Touch为我们提供了诸如delegate(dataSource)blocktarget/action这些盲通信方式。

Model指向Controller的同样也是虚线。原因也差不多,Model层代表的数据层应该与Controller无关。当Model改变的时候,通过KVO或者Notification的方式来通知Controller应当更新View。

这里有一点要提一下:UIViewController往往用来作为MVC中的Controller,MVC中的Controller也可以由其他类来实现


问题

通过上文的讲解,我们可以看到在纯粹的MVC设计模式中,Controller不得不承担大量的工作:

  • 网络API请求
  • 数据读写
  • 日志统计
  • 数据的处理(JSON<=>Object,数据计算)
  • 对View进行布局,动画
  • 处理Controller之间的跳转(push/modal/custom)
  • 处理View层传来的事件,返回到Model层
  • 监听Model层,反馈给View层

于是,大量的代码堆积在Controller层中,MVC最后成了Massive View Controller(重量级视图控制器)。

为了解决这种问题,我们通常会为Controller瘦身,也就是把Controller中代码抽出到不同的类中,引入MVVM就是为Controller瘦身的一个很好的实践。


MVVM

在MVVM设计模式中,组件变成了Model-View-ViewModel。

MVVM有两个规则

  • View持有ViewModel的引用,反之没有
  • ViewModel持有Model的引用,反之没有

图中,我们仍然以实线表示持有,虚线表示盲通信。

在iOS开发中,UIViewController是一个相当重要的角色,它是一个个界面的容器,负责接收各类系统的事件,能够实现界面专场的各种效果,配合NavigationController等能够轻易的实现各类界面切换。

在实践中,我们发现UIViewControllerView往往是绑定在一起的,比如UIViewController的一个属性就是view。在MVVM中,Controller可以当作一个重量级的View(负责界面切换和处理各类系统事件)。

不难看出,MVVM是对MVC的扩展,所以MVVM可以完美的兼容MVC。

对于一个界面来说,有时候View和ViewModel往往不止一个,MVVM也可以组合使用:


Controller解耦

MVC是一个优秀的设计模式,本文讲解MVVM也不是说想要用MVVM来替代MVC。对于软件设计来说,设计模式仅仅是一些参考工具,并没有固定的范式,使用起来是很灵活的。MVVM的很多理念对于Controller解耦是很有帮助的。

SubView

把相关的View放到一个Container View里,这样把对应View的创建,Layout等代码抽离出来,并且由Container统一处理用户交互,回调给外部。(这个比较好理解,就不举例子了)


Layout

在iOS中,视图的Layout一直是代码很乱的一块。通常Layout有两种

  • 手动的计算Frame - 简单粗暴,但是修改起来困难,易读性也不好
  • 通过约束AutoLayout - 有学习成本,并且不好debug,但是修改起来方便,也容易阅读。

通常使用Autolayout,我们都会用一些DSL的三方库:Masonry(OC),SnapKit(Swift)。

以一个常见的Layout为例,以下两图是在一个App中很常见的两种TableViewCell Layout:

两行列表

左边图,右边detail

这里,我们只关心左侧的图,在常规的Layout情况下Cell中的代码:

//Swift代码,使用SnapKit
leftImageView = UIImageView(frame: CGRect.zero)
contentView.addSubview(rightLabel)
//Layout
leftImageView.snp.makeConstraints { (maker) in
    maker.leading.equalTo(contentView).offset(8.0)
    maker.width.height.equalTo(80)
    maker.centerY.equalTo(contentView)
}

于是,两种cell类中,我们把上述代码进行Copy Paste。

那么有没有一种更好的方式进行Layout复用呢?

其实有两种方式进行Layout复用:

  • 继承(由基类提供Layout) 个人不喜欢继承,继承带来的额外的耦合会造成后期维护牵一发而动全身。
  • Layout独立抽离出来,以协议的方式进行依赖。

这里以第二种方式为例:

首先定义一个协议:来定义可以用来布局

protocol Layoutable {
    func layoutMaker() ->(ConstraintMaker) -> Void
}

然后,对UIView进行扩展,增加布局方法,同时对于client端隐藏snapKit

extension UIView{
    func makeLayout(_ layouter:Layoutable) {
        snp.makeConstraints(layouter.layoutMaker())
    }
}

然后,我们定义一个结构体,来表示左侧的正方形布局

struct LeftSquareLayout : Layoutable {
    func layoutMaker() -> (ConstraintMaker) -> Void {
        return { maker in
            maker.leading.equalTo(self.superView).offset(8.0)
            maker.width.height.equalTo(self.length)
            maker.centerY.equalTo(self.superView)
        }
    }
    var length :CGFloat
    var superView : UIView
    init(length: CGFloat, superView:UIView) {
        self.length = length
        self.superView = superView
    }
}

于是,左侧图片的Layout代码变成了如下:

leftImageView.makeLayout(LeftSquareLayout(length: 80, superView: contentView))

工厂

工厂是一个很好的设计模式,你是否不断的在代码里重写类似的代码:

let titleLabel = UILabel(frame: CGRect.zero)
titleLabel.font = UIFont.systemFont(ofSize: 14)
titleLabel.textColor = UIColor(colorLiteralRed: 0.3, green: 0.3, blue: 0.3, alpha: 1.0)
titleLabel.text = "Inital Text"
contentView.addSubview(titleLabel)

一般App的字体的大小和颜色都是几种之一,这时候我们用工厂的方式生产实例,能更好的实现代码复用:

定义Label类型:

enum LabelStyle {
    case title
    case subTitle
}

定义工厂方法:

extension UILabel{
    static func with(style initalStyle:LabelStyle) -> UILabel{
        switch initalStyle {
        case .title:
            let titleLabel = UILabel(frame: CGRect.zero)
            titleLabel.font = UIFont.systemFont(ofSize: 14)
            titleLabel.textColor = UIColor(colorLiteralRed: 0.3, green: 0.3, blue: 0.3, alpha: 1.0)
            return titleLabel
        default:
            return UILabel()
        }
    }
}

我们还可以提供两个方法,能够让我们链式的添加到superView和config

extension UILabel{
    @discardableResult
    func added(into superView:UIView) -> UILabel{
        superView.addSubview(self)
        return self
    }
    @discardableResult
    func then(config:(UILabel) -> Void) ->UILabel{
        config(self)
        return self
    }
}

于是,代码变成了这样子

UILabel.with(style: .title).added(into: contentView).then { $0.text = "Inital Text"}

在结合上文的Layout,我们甚至可以用一个链式的调用完成初始化和Layout

UILabel.with(style: .title)
    .added(into: contentView)
    .then { $0.text = "Inital Text"}
    .makeLayout(yourLayout)

Note: 仅仅举例,实际应用中,你可以需要更好的去设计语法

链式调用的延伸阅读:PromiseKit


ViewModel

在MVC的Controller解耦中,引入ViewModel是一种很常见的方式。把Controller中对应与View相关的逻辑层出来,这样Controller需要做的就是

  • 从DB/网络中获取数据,转换成ViewModel
  • 把ViewModel装载给View
  • View的属性与ViewModel值绑定在一起(单向)

在Swift中,实现单向绑定是很容易的:

定义一个可绑定类型:

class Obserable<T>{
    typealias ObserableType = (T) -> Void
    var value:T{
        didSet{
            observer?(value)
        }
    }
    var observer:(ObserableType)?
    func bind(to observer:@escaping ObserableType){
        self.observer = observer
        observer(value)
    }
    init(value:T){
        self.value = value
    }
}

然后,我们扩展UILabel,让其text能够绑定到某一个Obserable值上

extension UILabel{
    var ob_text:Obserable<String>.ObserableType {
        return { value in
            self.text = value
        }
    }
}

接着,建立一个ViewModel

class MyViewModel{
    var labelText:Obserable<String>
    init(text: String) {
        self.labelText = Obserable(value: text)
    }
}

然后,就可以这么用单向绑定了

let label = UILabel()
let viewModel = MyViewModel(text: "Inital Text")
viewModel.labelText.bind(to: label.ob_text)

//修改viewModel会自动同步到Label

viewMoel.labelText.value = "New Text"

当然,实际使用MVVM的时候,手动实现绑定和View事件回调也可以。

延伸阅读:


网络

网络请求的代码往往也是放到UIController的生命周期里(比如viewDidLoad)或者某些用户的UI操纵。假设你基于以下三个开源库框架进行网络请求和JSON解析

我们来模拟一个登录的网络请求,首先定义一个数据结构表示登录的结果

struct LoginResult: Mappable{
    var token: String
    var name: String
    init?(map: Map) {/* */}
    mutating func mapping(map: Map) {
        name <- map["name"]
        token <- map["token"]
    }
}

然后,在button点击事件中,进行login

func handleLogin(sender:UIButton){
    let userName = "userName"
    let passWord = "password"
    let url = "https://api.example.com/user/login"
    let params = ["username":userName,"password":passWord]
    Alamofire.request(url, method: .post, parameters: params, encoding: JSONEncoding()).responseObject { (response:DataResponse<LoginResult>) in
        guard let result = response.value else{
            print(response.error ?? "Unknown Error")
            return
        }
        print(result.name)
        print(result.token)
    }
}

这是一个很常规的做法:

  • 在Controller中获取网络请求需要的数据
  • 把请求数据给网络模块,网络模块负责请求网络数据,并且解析成对象,然后异步回调给Controller
  • 在Controller中处理网络模块回调的结果

这么做有两个问题

  1. host,paramter encoding等相关信息对Controller应当透明
  2. Controller不应该知道网络层是基于Alamofire的

于是,这里我们把网络层抽离:

首先,定义一个协议,表示能够解析成一个网络请求的类型:

protocol NetworkAPIConvertable {
    var host:String {get}
    var path:String {get}
    var method:RequestMethod{get}
    var requestEncoding:RequestEncoding{get}
    var requestParams:[String:Any]{get}
}

其中,RequestMethod和RequestEncoding是对Alamofire的简单封装

enum RequestEncoding{
    case json, propertyList, url
}
enum RequestMethod{
    case get, post, delete, put
}
private extension RequestMethod{
    func toAlamofireMethod()->HTTPMethod{
        switch self {
        case .get:
            return .get
        case .post:
            return .post
        case .delete:
            return .delete
        case .put:
            return .put
        }
    }
}
private extension RequestEncoding{
    func toAlamofireEncoding()->ParameterEncoding{
        switch self {
        case .json:
            return JSONEncoding()
        case .propertyList:
            return PropertyListEncoding()
        case .url:
            return URLEncoding()
        }
    }
}

接着,定义请求的接口

struct APIRouter{
   static func request<ResponseType:Mappable>(api:NetworkAPIConvertable,completionHandler:@escaping (ResponseResult<ResponseType>) -> Void){
        let requestPath = api.host + "/" + api.path
        _ = Alamofire.request(requestPath,
                          method: api.method.toAlamofireMethod(),
                          parameters: api.requestParams,
                          encoding: api.requestEncoding.toAlamofireEncoding())
            .responseObject { (response:DataResponse<ResponseType>) in
                            if let value = response.value{
                                completionHandler(ResponseResult.succeed(value: value))
                            }else{
                                completionHandler(ResponseResult.error(error: response.error ?? NSError(domain: "com.error.unknown", code:-1, userInfo: nil)))
                            }
    }
    }
}

enum ResponseResult<Value>{
    case succeed(value:Value)
    case error(error:Error)
}

于是,我们的网络层封装基本完成了。然后,我们来定义我们的login API

enum NetworkService{
    case login(userName:String,password:String)
    //Add what you need
}
extension NetworkService: NetworkAPIConvertable{
    var host: String {
        return "https://api.example.com"
    }
    var requestEncoding: RequestEncoding {
        switch self {
        case .login(_,_):
            return .json
        }
    }

    var requestParams: [String : Any] {
        switch self {
        case .login(let userName, let password):
            return ["username":userName,"password":password]
        }
    }

    var path: String {
        switch self {
        case .login(_,_):
            return "user/login"
        }
    }

    var method: RequestMethod {
        switch self {
        case .login(_,_):
            return .post
        }
    }
}

接着,网络请求变成了

let userName = "userName"
let passWord = "password"
let login = NetworkService.login(userName: userName, password: passWord)
APIRouter.request(api: login) { (response:ResponseResult<LoginResult>) in
    switch response{
    case .succeed(let value):
            print(value.token)
    case .error(let error):
            print(error)
    }
}

延伸阅读:Moya


日志

大部分App都会做日志分析,于是你的代码中不得不进行埋点:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    //发送日志
    Logger.collectWithContent(....)
}

当你看这样的代码的时候,日志代码也在看着你:

是不是很痛苦呢?

在抽离日志之前,我们想想什么样的日志模块是我们想要的?

  • 尽量不要侵入业务代码
  • 支持由后台动态下发日志统计内容

AOP是一种常见的日志统计解决:

通过AOP的方式hook所有需要统计的UIView事件回调,然后通过KVC的方式来获取日志需要的数据,是常见的无埋点日志解决方案。

比如很常见的友盟统计需要在viewWillAppear/viewWillDisappear中加入代码:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [MobClick beginLogPageView:@"Page1"];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [MobClick endLogPageView:@"Page1"];
}

使用AOP的方式,代码变成如下:

void swizzle(Class cls,SEL originalSEL,SEL swizzledSEL){
    Method originalMethod = class_getInstanceMethod(cls, originalSEL);
    Method swizzledMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

@implementation UIViewController (QTSwizzle)
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        swizzle(self.class, @selector(viewWillAppear:), @selector(sw_viewWillAppear:)));
        swizzle(self.class, @selector(viewWillDisappear:), @selector(sw_viewWillDisappear:)));
    });
}
- (void)qt_viewWillAppear:(BOOL)animated{
    [self qt_viewWillAppear:animated];
    // Log代码
}
- (void)qt_viewWillDisappear:(BOOL)animated{
    [self qt_viewWillDisappear:animated];
    // Log代码
}
@end                        

可以看到,我们通过AOP,在原有的viewWillAppear后动态插入的日志代码,其他点击事件也可以类似处理。另外,Objective C有一个很方便的用来做AOP的开源框架:Aspects

细心的同学可能看到了,这块的代码我是以Objective C作为例子的,因为OC的Runtime特性,可以很方便的做AOP。对于NSObject及其子类,Swift也支持AOP,但是考虑到Swift的语言特性,关于Swift的无侵入日志,也许还可以方案:

  • 一套支持日志统计的框架。这个看起来工作量很大,但其实需要做大量日志统计的公司往往都有自己的一套XXUIKit,在基类里加入日志统计的基础逻辑也未尝不可
  • 编译期AOP。这个仅局限于理论,就是

延伸阅读:


数据存储

iOS常用的本地数据存储方案有几种:

  • UserDefaults 用户配置信息
  • File/Plist 少量的无须结构化查询的数据
  • KeyChain 密码/证书等用户认证数据
  • 数据库 需要结构化查询的信息
  • iCloud

而数据库往往是App的数据核心。在iOS中:可以选择数据库技术有

  • CoreData - 对应开源库MagicalRecord
  • Sqlite直接封装 - 对应开源库 FMDB
  • Realm

CoreData的坑比较多,想要用好需要比较高的学习成本。Relam和Sqlite都是建立结构化查询数据库的比较好的选择。

使用FMDB,你的代码类似这样子的。

let queue = FMDatabaseQueue(url: fileURL)

queue.inTransaction { db, rollback in
    do {
        try db.executeUpdate("INSERT INTO foo (bar) VALUES (?)", values: [1])
        try db.executeUpdate("INSERT INTO foo (bar) VALUES (?)", values: [2])
    } catch {
        rollback.pointee = true
    }
}

可以看到,FMDB是把sqlite从C的API封装成了Objective/Swfit等上层API。但是还是缺少了两项比较核心的

  • ORM(Object Relational Mapping)从数据库的表映射到Structs/Class
  • 查询语言。在代码里进行SQL字符串的编写是繁琐的也容易出问题

于是,通常你需要在FMDB(Sqlite)上在进行一层封装,这一层封装提供ORM和查询语言。从而更有好的提供上层接口。类似的框架有:

延伸阅读:


路由

在iOS开发中,UIViewController之间的跳转是无法避免的一个问题。比如,一个ViewControllerA想要跳转到ViewControllerB

#import "ViewControllerB.h"

//...
ViewControllerB * vcb = [[ViewControllerB alloc] init];
[self.navigationController pushViewController:vcb animated:YES];

当在一个类中import另一个类的时候,这两个类就形成了强耦合。

另外,很多App都有一个用户中心的界面,这个界面有一些特点就是会跳转到很多界面。于是,日积月累,这个类中,你会发现代码编程了这个样子:

if indexPath.secion == 0{
    if indexPath.row == 0{

    }else if....
}else if indexPath.section == 1{

}
....

大量的if/else造成代码难以阅读,并且难以修改。

一个典型的解Controller与Controller解耦方案就是加一个中间层:路由,并且建立Module(模块)来管理一组Controller。

类似这种的路由架构,在App启动的时候,通过注入的方式把各个Module

一个典型的跳转请求如下:

  • ControllerA发起跳转请求Request
  • Router解析Request,轮询问各个Module,看看各个Module是否支持对应的Requst。
    • 如果有则把requst转发给对应的Module;
    • 如果没有,根据Request的内容可选请求远端服务器,服务器可能返回H5地址
  • Router根据远端服务器,或者Module的Response,合成跳转的command,发送给导航模块
  • 导航模块根据command进行跳转,并且返回feedBack给Router
  • Router返回feedback给ControllerA

总结

iOS App是一个麻雀虽小,五脏俱全的软件。良好的架构和设计能够让代码容易理解和维护,并且不易出错。关于App的设计一个仁者见仁,智者见智的问题,并没有什么固定的范式。本文也只是提出了笔者的一些经验,仅供参考,

另外,本文是一篇公司内部分享文章,事实上在公司内部分享的时候,并没有这么完善,后面我有陆陆续续利用空闲时间写了几天,如有读者发现问题,欢迎评论或者GitHub issue,这篇博文会放在我的github上。

作者:Hello_Hwc 发表于2017/6/12 11:20:19 原文链接
阅读:729 评论:1 查看评论

Unity 多物体联动动画

$
0
0

分享一个前几天写的插件,当时为了做多个物体的简单动画(只有移动、旋转、缩放之类的)同时运动效果而写的,说白了算不上什么高级的联动动画,就只是同时控制多个物体协调运动而已,像什么机械类的原理动画展示,类似于齿轮传动机构之类的。


当时用自带的Animation调关键帧动画的时候,由于不停的在多个物体间切换,用过的都知道,那蛋疼的酸爽险些让我直接砸了电脑。所以之后才想着直接用插值动画写个多物体协调运动的插件,当然主要是针对一些简单的动画,量大的动画,以及有可能有数十个物体同时在运动的动画。


好了,先看下比较简单的使用方式:在场景中任意物体上挂载LinkageAnimation脚本,一个LinkageAnimation实例只对应一组动画。



Target Number表示当前动画组中有多少个动画物体,Frame Number表示当前动画组的关键帧数量,一个动画组中的所有联动物体拥有相同的关键帧数量。

Pause:暂停状态,勾选之后动画开启。

Speed:动画速度,值越大越快。

Add Target:添加动画联动物体,新增的物体会自动拥有当前已存在的关键帧数量。

Add Frame:添加关键帧,新增的关键帧会附加给每一个联动物体。

当前选中Frame0,也就是选中了关键帧0,在关键帧0下,可以查看当前关键帧每个联动物体的被记录组件的属性,目前只有Transform组件的属性被动画关键帧记录。

Delete:从动画组中删除某一联动物体,同时删掉他的所有关键帧。

Forbid:是否禁用此条属性,禁用后,在动画组运行其间,此关键帧与他的前后两帧的该属性都不会产生动画效果,默认状态所有属性都被禁用。

Get:从场景中获取物体此时的该属性的值,最好的方式就是在场景中将物体调节到指定的状态,然后新增关键帧,同时获取并保存他的属性。

Delete Frame:选中某一帧时,此按钮为删除此帧。


当完成一个动画组的关键帧制作之后,我们勾选Playing,运行场景就可以看到效果。



这是三个物体的联动动画,共包含五个关键帧:




动画的原理即是数值插值,然后我直接贴出整个LinkageAnimation的源码:


using UnityEngine;
using System.Collections.Generic;

public class LinkageAnimation : MonoBehaviour
{
    public bool Playing = true;
    public float Speed = 1.0f;
    public List<AnimationFrame> Targets;

    [SerializeField]
    private int _frameLength = 0;
    private int _frameIndex = 0;
    private float _frameLocation = 0.0f;

#if UNITY_EDITOR
    /// <summary>
    /// 添加联动物体
    /// </summary>
    public void AddAnimationFrame()
    {
        if (Targets == null)
            Targets = new List<AnimationFrame>();

        AnimationFrame af = new AnimationFrame(transform);
        for (int i = 0; i < _frameLength; i++)
        {
            af.Frames.Add(new Frame());
        }
        Targets.Add(af);
    }

    /// <summary>
    /// 移除联动物体
    /// </summary>
    public void RemoveAnimationFrame(AnimationFrame frame)
    {
        if (Targets == null)
            return;

        if (Targets.Contains(frame))
        {
            Targets.Remove(frame);
            if (Targets.Count <= 0)
                _frameLength = 0;
        }
    }

    /// <summary>
    /// 移除联动物体
    /// </summary>
    public void RemoveAtAnimationFrame(int index)
    {
        if (Targets == null)
            return;

        if (index >= 0 && index < Targets.Count)
        {
            Targets.RemoveAt(index);
            if (Targets.Count <= 0)
                _frameLength = 0;
        }
    }

    /// <summary>
    /// 添加关键帧
    /// </summary>
    public void AddFrame()
    {
        if (Targets == null || Targets.Count <= 0)
            return;

        for (int i = 0; i < Targets.Count; i++)
        {
            Targets[i].Frames.Add(new Frame());
        }
        _frameLength += 1;
    }

    /// <summary>
    /// 移除关键帧
    /// </summary>
    public void RemoveAtFrame(int index)
    {
        if (Targets == null || Targets.Count <= 0)
            return;

        for (int i = 0; i < Targets.Count; i++)
        {
            if (index >= 0 && index < Targets[i].Frames.Count)
            {
                Targets[i].Frames.RemoveAt(index);
            }
        }
        _frameLength -= 1;
    }

    public int FrameLength()
    {
        return _frameLength;
    }
#endif

    private void Update()
    {
        UpdateAnimation();
    }

    private void UpdateAnimation()
    {
        if (Playing && Targets != null && _frameLength > 1)
        {
            if (_frameLocation >= 1.0f)
            {
                _frameLocation = 0.0f;
                _frameIndex += 1;
                if (_frameIndex >= (_frameLength - 1))
                {
                    _frameIndex = 0;
                }
            }
            else
            {
                _frameLocation += Time.deltaTime * Speed;
            }

            for (int i = 0; i < Targets.Count; i++)
            {
                if (Targets[i].Target != null && Targets[i].Frames.Count > 0)
                    UpdateFrame(Targets[i]);
            }
        }
    }

    private void UpdateFrame(AnimationFrame af)
    {
        if (!af.Frames[_frameIndex].PositionDisabled && !af.Frames[_frameIndex + 1].PositionDisabled)
            af.Target.localPosition = Vector3.Lerp(af.Frames[_frameIndex].Position, af.Frames[_frameIndex + 1].Position, _frameLocation);
        if (!af.Frames[_frameIndex].RotationDisabled && !af.Frames[_frameIndex + 1].RotationDisabled)
            af.Target.localRotation = Quaternion.Euler(Vector3.Lerp(af.Frames[_frameIndex].Rotation, af.Frames[_frameIndex + 1].Rotation, _frameLocation));
        if (!af.Frames[_frameIndex].ScaleDisabled && !af.Frames[_frameIndex + 1].ScaleDisabled)
            af.Target.localScale = Vector3.Lerp(af.Frames[_frameIndex].Scale, af.Frames[_frameIndex + 1].Scale, _frameLocation);
    }
}


以及动画帧类:


[System.Serializable]
public class AnimationFrame
{
    public Transform Target;
    public List<Frame> Frames;

    public AnimationFrame( Transform tf )
    {
        Target = tf;
        Frames = new List<Frame>();
    }
}

[System.Serializable]
public class Frame 
{
    public Vector3 Position;
    public Vector3 Rotation;
    public Vector3 Scale;
    public bool PositionDisabled;
    public bool RotationDisabled;
    public bool ScaleDisabled;
    public bool ShowInspector;

    public Frame()
    {
        Position = Vector3.zero;
        Rotation = Vector3.zero;
        Scale = Vector3.one;
        PositionDisabled = true;
        RotationDisabled = true;
        ScaleDisabled = true;
        ShowInspector = false;
    }
}


作者:qq992817263 发表于2017/6/12 14:06:36 原文链接
阅读:19 评论:0 查看评论

带你解锁蓝牙skill(二)

$
0
0

本文已授权微信公众号fanfan程序媛独家发布

紧接着带你解锁蓝牙skill(一)继续分析
转载请注明出处,本文出自fanfan带你解锁蓝牙skill(二)


3>,Pbap配置

Pbap:Phone Book Access Profile是说共享联系人协议,具体定义可参考上一篇。
其实说白了就是一个手机可以从另一个手机中导入联系人信息。
其中包括两个角色

  1. PCE:对应的协议配置文件为PbapClientProfile,作为客户端,是指获取联系人的一端
  2. PSE:对应协议配置文件为PbapServerProfile,作为服务器端,是指提供联系人数据的一端。

Android源码在7.0以前只支持作为PSE即作为提供数据的一方。但是各芯片提供商可能会自己增加一个蓝牙协议用于支持读取数据。



现在用两个手机做测试机进行测试

  • 一个是支持PSE,PCE两种角色,相当于既可以当客户端又可以当服务端(即既可以提供联系人数据,又可以获取对方的联系人数据),可以主动选择获取其他手机上的联系人数据。我们称之为S&C(server&client).
  • 一个是只支持PSE(即作为提供联系人数据的一方),只能作为服务端,不能通过蓝牙获取其他手机上的联系人,只能是被动的提供数据。我们称之为S(server)。



首先看一下Pbap是怎么用的。

第一,Pbap不是什么

首先要说明一点:Pbap不是说在通讯录中通过蓝牙分享联系人,因为在分享联系人时是将联系人先导出到一个一个文件,然后把文件分享出去,归根结底还是分享文件,走的是Opp协议。如下图所示,当对方在分享一个联系人信息时,会先将联系人打包到一个vcf文件中,并通过文件分享的方式传送过来

这里写图片描述



那么Pbap是什么呢??

第二,Pbap是什么

在通讯录–>菜单中,会有导入/导出选项,针对Pbap的两种角色来进行分析。

1>,如果你的手机只支持PSE角色不支持PCE,即只支持外界读取本机的联系人,不支持读取外界联系人的话—–测试机S ,那么显示选项如下

导入导出

联系人的来源只有一个,那就是从存储设备中导入。(因为没有安装sim卡的缘故)
其实导入来源应该是有两个

  • 从存储设备中导入
  • 从sim卡中导入

2>,但如果你的手机既支持PSE角色有支持PCE角色的话—–测试机S&C,那么导入联系人时的选项如下

这里写图片描述

可以看到对于联系人的导入可以通过三种

  • 从存储设备中导入
  • 从sim卡中导入
  • 从其他手机导入

相对于上一种情况,这种条件下多了一个从其他手机导入的支持。
接下来就可以从其他设备导入通讯录了

这里写图片描述

选择通过蓝牙方式导入联系人后,会提示

这里写图片描述

在从其他设备导入联系人时有两个要求,

  • 保证对方设备在设置中开启蓝牙
  • 报这个对方设备设置为对其他蓝牙可见

这两个条件是为了让S&C设备可以实现和C设备的配对。配对成功后,在C设备上会有一个读取联系人权限的问题

这里写图片描述

权限声明如下:
设备想要访问您的联系人信息和通话记录。要向设备授予访问权限吗?
点击同意之后,C设备上的联系人信息便会同步到S&C设备上。C设备上的联系人会成为S&C设备上联系人的一部分。所获取到的信息包括联系人信息和通话记录。

当然,因为我的测试机是S&CC,所以以上就是按这种情况考虑的。具体情况基本可以类推。

举个例子来帮助你理解Pbap的含义。
比如现在有3个桶A,B,C,其中桶C是空的。桶A和桶B有水,而且桶B无限大(哈哈,搞笑嘛,谁不会,哈哈哈)。
接下来桶A主动将桶A的水的一部分(某个联系人)倒入到桶C中(打包成一个vcf文件),然后送给桶B(这种情况就是C设备通过蓝牙分享的方式将联系人分享至S&C设备),此时蓝牙分享联系人过程已经结束,在桶B这里需要自己把桶C中的水倒进来(即S&C设备获取到了一个vcf文件)。这种情况并不是属于Pbap的情况

但如果桶B说我要获取A的所有水(导入联系人),也就是说桶A是被动提供水的,此时就是S&C设备通过蓝牙导入联系人,那么A中的所有水会直接倒入C中(所有联系人打包到一个vcf文件),并且,此时还没结束,桶C中的水紧接着会倒入B中与B融为一体,至此,通过蓝牙导入联系人的过程才结束。

写了那么多,该歇一歇了
这里写图片描述

哇噻,有彩虹噢~~~

4>,音频协议

Android中的音频协议包括两大类

  1. 手机音频:蓝牙通话,常见的是蓝牙耳机。协议是HeadSet
  2. 媒体音频:蓝牙播放音视频声音,可以是手机,蓝牙耳机,蓝牙音箱等可以播放音频的设备,相关协议是A2dp
    如下图所示,手机与一个蓝牙耳机进行配对时会显示手机音频和媒体音频配置项。

这里写图片描述

先来说一下媒体音频相关—A2dp,A2dp是说音频分发协议,分为两个角色

  • Source (SRC) : A2dp服务端,用于提供音频源,对应的协议文件为A2dpProfile。
  • Sink (SNK) :A2dp客户端,用于播放音频,对应的协议文件为A2dpSinkProfile.
    其实说白了A2dp的高级音频分发协议说的就是用其他蓝牙设备播放本机的媒体音频(注意,是音频,只有声音)。

再来看看手机音频—-Headset,与A2dp类似,同样分为两个角色

  • HeadsetProfile:提供音频源的一端
  • HfpClientProfile:播放音频源的一端

比如在手机和蓝牙耳机的连接中
蓝牙耳机可以作为媒体音频的播放器—A2DP的sink角色,也可以作为手机音频的播放器—Headset的HfpClient
而手机扮演的是媒体音频中的音频源的提供者—A2dp的src角色,手机音频中的音频提供者–headset。

转载请注明出处,本文出自fanfan带你解锁蓝牙skill(二)

至于HID和Map一个是人机接口设备,一个是读取短彩信,因条件不允许,暂时不能演示,接下来进行源码研究。
如有疑问,可扫描屏幕右下方二维码关注微信公众号fanfan程序媛:fanfan程序媛

作者:zrf1335348191 发表于2017/6/12 17:11:39 原文链接
阅读:4800 评论:0 查看评论

HBuilder webApp开发(十六)定位geolocation

$
0
0

HBuilder的geolocation定位模块其实接口很少,通常我们使用定位模块即可。
这里写图片描述

var posi = null;
mui.plusReady(function() {
    posi = plus.geolocation.getCurrentPosition( function ( p ) {
        console.log( "Geolocation\nLatitude:" + p.coords.latitude + "\nLongitude:" + p.coords.longitude + "\nAltitude:" + p.coords.altitude );
        console.log("street = " + p.address.street);
        console.log("poiName = " + p.address.poiName);
        console.log("coordsType = " + p.coordsType);
        console.log("timestamp = " + p.timestamp);
        console.log("addresses = " + p.addresses);
    }, function ( e ) {
        console.log( "Geolocation error: " + e.message );
    },{enableHighAccuracy: true}); 
});
mui.back = function(){
    var btn = ["确定","取消"];
    mui.confirm('确认关闭当前窗口?','Hello MUI',btn,function(e){
        if(e.index==0){
            // 关闭位置监听  并返回
            plus.geolocation.clearWatch( posi );
            mui.currentWebview.close();
        }
    });
}
作者:zhuming3834 发表于2017/6/13 15:42:51 原文链接
阅读:44 评论:0 查看评论

HBuilder webApp开发(十七)百度地图URI API的使用

$
0
0

年前的使用公司做了一个物流的app,目的是方便送货师傅的使用。在做的时候,我就集成了百度地图URI API。这个使用比较简单,这里我使用的地址解析baidumap://map/geocoder,这个接口。但是在使用时要区分iOS和安卓系统,就这唯一一个注意点,参数也就是一个,前提是手机安装的有百地图app。

document.getElementById('map').addEventListener('tap',function(){
    // 调起百度地图 
    // http://lbsyun.baidu.com/index.php?title=uri/api/android
    // iOS端
    var href = "baidumap://map/geocoder?address=目的地址&src=webapp.geo.yourCompanyName.yourAppName";
    // Android端
    var anhref = "bdapp://map/geocoder?address=目的地址&src=openApiDemo"
    var osName = plus.os.name;
    if (osName == "iOS") {
        window.open(href);
    } else{
        window.open(anhref);
    }
})

参数address,填写你的目的地址,如:大冲商务中心。
这里写图片描述

作者:zhuming3834 发表于2017/6/13 15:58:33 原文链接
阅读:49 评论:0 查看评论

iOS 与 Android 系统十年之战,究竟谁是赢家?

$
0
0

2007 年,功能机巨头林立的时代,苹果在 Macworld 大会上公布了 iOS 系统。同一年,Google 与多家制造商、开发商、电信运营商和芯片制造商联合创立开放手持设备联盟(OHA)共同研发改良 Android 系统。

这是移动互联网浪潮的起始。

2014 年前,当移动互联网风头正盛之时,有人预言「移动互联网的台风已经结束,下一个机会将转向智能领域」,信者寥寥。但如今,各式智能硬件层出不穷,且因着 AlphaGo 战胜人类的惊世对局,科技圈街头巷尾我们皆可闻「人工智能」。

属于移动互联网的红利时代到这里结束。

比尔盖茨言「我们总是高估短期能够做到的,而低估五年或者十年中能够做到的」,但在这个技术日新月异的时代,又能有几个五年或十年?

如今回头来看,移动端操作系统十年大战的格局已相当明朗,最后只剩下了两大巨头。

但苹果一贯的封闭和精品策略,决定了苹果仅仅是一个人在战斗,也没有向任何盟友伸出橄榄枝。完全依赖手机一年一部的发行节奏来驱动市场,通过在硬件外观上的创新来撩拨消费者的心弦,这样的封闭策略已注定了 iOS 不会主导整个市场份额。

而 Google 天生的开放策略,为自家的操作系统和生态系统建设奠定了基调。通过建立开放的 OHA 和免费授权手机厂商自由定制操作系统,迅速地拉起了 Android 联盟,各式基于 Android 深度定制的 ROM 遍地开花。但这种现象无疑是把双刃剑,一方面,由于众多厂商的参与,在硬件上完全发挥了各自的想象力和创造力,为最终用户提供了更多的选择和机会;而另一方面,也造就了 Android 系统臭名昭著的机型适配和系统分裂问题。尽管如此,几乎绝大部分的手机厂商还是跟随了 Google 的脚步,从而使得 Google 占据的智能手机市场份额节节攀升。

几天前,在 CNET 上看到一篇题为“iOS 11 vs. Android O: Who’s winning so far?”的文章,提到这样一个问题:

苹果公司刚刚为 iPhone 发布了 iOS 11,而 Google 几周前也更新了 Android O。这两种系统几乎支持全球所有手机,那么,谁才是现阶段的赢家?

答案给得很干净利落 —— Android O,同时也对各自领先与落后的地方进行了具体分析,在此分享给大家。

1. 最炫酷的新功能

Android O:

  • 可以将视频或谷歌地图导航缩小为漂浮缩略图,以便你在查看它们的同时还可以继续做其他的事情(这种功能被称为“画中画”);
  • 可自动选择名字、电话号码、地址或商务信息复制并粘贴;
  • 全新的应用通知功能,你可以按住应用图标扩展信息,浏览警示内容。

iOS 11:

  • 用 Apple Pay 在 iMessage 中向朋友支付;
  • 可以播放室内多个扬声器;
  • 这里可以浏览 iOS11 的所有全新功能。

目前赢家:Android O。“画中画”与自动填充都属于细节改进,但从长期来看却能让手机的日常使用更加便捷。与 iOS 不同, Android 手机早就可以通过手机控制多个扬声器播放音乐(但必须与 Chromecast 兼容)。

然而,iMessage 中的 P2P 支付功能是对自带聊天软件的很好补充(这一点 Android 所有手机都不具备)。

2. 语音助理

苹果有 Siri,Android 有 Android Assistant(还有 Google Voice Search 和 Google Now)。Siri 的许多更新让它看起来变得更好,比如新加的男性声音、猜测你接下来想知道什么等。而 Google Assistant 的 Google Lens 将具备可识别物体的模式,它还拥有键盘输入查询功能,而不仅仅是说出来。

目前赢家:Android O(虽然新增的语音功能没什么新意)。与 iOS 11 相比,Android O 的底层平台更加广阔,Google Assistant 也比 Siri 更准确,拥有更广的知识面。在苹果全球开发者大会(WWDC)上,苹果错过了展示其智能助理引发轰动的绝佳机会。

3. 消息应用

iOS 11 将 iMessage 打造成“应用抽屉”,而增加 Apple Pay 功能是个大亮点。谷歌 Android 手机上缺少这类集多种功能于一体的消息应用,Android Messages 是标准的(且基础的)发短信应用程序,Hangouts、Allo 以及 Duo 也各有特定用途。

虽然 Android 在 Google Wallet 中也有 P2P 支付功能,但需要另外单独安装和设置应用。

目前赢家:iOS 11。iOS 用户可以使用大量特殊功能,比如文字特效、无缝 Wi-Fi、SMS 短信以及最新的信息内 P2P 支付功能等。

4. 具体细节

由于苹果自身控制着其硬件和软件,它不需要像谷歌那样为数十个完全不同的手机品牌的上百种手机开发统一的操作系统。但这也意味着,谷歌在打造更快操作系统、提高电池续航时间等方面能够更积极地分享其成果与进度。

目前赢家:可能是 Android O。与去年操作系统相比,谷歌宣称 Android O 操作系统的速度比去年提高了 1 倍,启动速度也更快。谷歌还努力改进后台工作,以节省电池电量。苹果并没有强调这些改进,但其设备也没有谷歌设备类似的问题。我们需要对苹果和谷歌下一代智能手机进行对比,才能知道电池续航能力和操作系统速度的实际情况。

5. 设计改进

苹果重新设计了 iOS 11 的锁屏、控制中心、Siri 界面以及应用商店。Android O 虽然也进行了视觉调整,但其仍无法与 iOS 相媲美。

目前赢家:iOS 11。iOS 11 目前的设计我们已经很满意了,特别是全新推出的控制中心,但我们对新的应用商店的实际效果保留怀疑。

6. AR 与 VR

Android 在 AR 和 VR 方面都比苹果领先很多年,Android 拥有支持 VR 的 Google Daydream 和支持 AR 的 Tango,最近甚至宣布推出独立头盔。苹果刚刚踏足 AR 领域,但苹果公司宣称其拥有世界上最大的 AR 平台,今年晚些时候将推出 ARKit 开发者平台。

目前赢家:VR 方面是 Android,AR 大战还未有结果。谷歌的 Tango 项目进展缓慢,如果苹果如传闻中所说其 iPhone 摄像头真的拥有支持 AR 功能,它将超越谷歌取得的进展。

7. 其他方面

苹果和谷歌都在人工智能(AI)方面投入大量资源进行研发,以便使其数字助理和其他工具更加智能,更有情境意识。

谷歌还正在推出 Instant Apps,即无需下载就可使用的迷你应用。它不属于 Android 的一部分,但同样也会进入手机领域。Google Lens 的概念令人印象深刻,它可以让你在其他东西中区分出飞行物体。

8. 把最好的留到最后?

iOS 与 Android 的较量还未结束。苹果和谷歌向来都有这样的传统,将最新操作系统部分功能留到新手机发布时宣布。

目前来看,谷歌在软件方面似乎更有优势,但苹果依然有机会赶超。据说苹果十周年版 iPhone 将会迎来巨大改变,许多最好的软件功能可能首次应用到它身上。

也就是说,苹果的 AR 代码可能在下一代 iPhone 摄像头上转变成杀手级功能。或者苹果可能完全放弃 iPhone 的 Home 键,更多免提 Siri 功能将脱颖而出。

苹果可能还保留着许多惊人功能,以待与新 iPhone 同时发布。除非 Android O 在未来的 Pixel 手机上也有惊人之举,否则可能被苹果反超。

对于所有这些预测,我们拭目以待。

作者:Byeweiyang 发表于2017/6/13 16:51:12 原文链接
阅读:1100 评论:1 查看评论

最新Swift学习教程-从简单到复杂 韩俊强的博客

$
0
0

Swift基础知识大全,Swift学习从简单到复杂,不断地完善与更新, 欢迎Star❤️,欢迎Fork,☀️iOS开发者交流群:446310206


知识架构:

  • 常两变量
  • 基本数据类型
  • 类型转换
  • Bool类型
  • 元祖
  • 可选值
  • 字符和字符串
  • 字符串常用方法
  • 运算符
  • 数组基本使用
  • 数组其它操作
  • 字典
  • if
  • while
  • for
  • break-continue
  • Switch
  • 函数定义
  • 函数参数
  • 函数类型
  • 闭包
  • 闭包捕获值
  • 枚举
  • 结构体
  • 属性
  • 方法
  • 下标subscripts

更新中...

iOS开发者交流群:446310206


作者:qq_31810357 发表于2017/6/13 17:10:13 原文链接
阅读:33 评论:0 查看评论

Android 神兵利器Dagger2使用详解(二)Module&Component源码分析

$
0
0

前言:

在我的上一篇文章 Android 神兵利器Dagger2使用详解(一)基础使用中,我们通过Dagger2依赖注入的两种方式获取Student对象,并简单了解了各个组件的作用和互相的联系:

@Inject : 注入,被注解的构造方法会自动编译生成一个Factory工厂类提供该类对象。

@Component: 注入器,类似快递员,作用是将产生的对象注入到需要对象的容器中,供容器使用。

@Module: 模块,类似快递箱子,在Component接口中通过@Component(modules =
xxxx.class),将容器需要的商品封装起来,统一交给快递员(Component),让快递员统一送到目标容器中。

本文我们继续按照上文案例来讲,通过源码分析,看看究竟是为什么,我们能够仅仅通过数个注解,就能随心所欲使用Student对象。

一 .代码回顾

我们先不考虑Module,还是这样的代码:

1 .Student类

public class Student {

    @Inject
    public Student() {
    }

}

2 .Module类

@Module
public class A01SimpleModule {

    private A01SimpleActivity activity;

    public A01SimpleModule(A01SimpleActivity activity) {
        this.activity = activity;
    }

}

3.Component类

@Component(modules = A01SimpleModule.class)
public interface A01SimpleComponent {

    void inject(A01SimpleActivity activity);

}

4.Activity类

public class A01SimpleActivity extends AppCompatActivity {

    @BindView(R.id.btn_01)
    Button btn01;

    @Inject
    Student student;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_a01_simple);
        ButterKnife.bind(this);
        //新添代码
        DaggerA01SimpleComponent.builder()
//                .a01SimpleModule(new A01SimpleModule(this))
                .build()
                .inject(this);
    }

    @OnClick(R.id.btn_01)
    public void onViewClicked(View view) {
        switch (view.getId()){
            case R.id.btn_01:
                Toast.makeText(this,student.toString(),Toast.LENGTH_SHORT).show();
                break;
        }
    }

然后运行代码,点击Button,输出student对象信息:

这里写图片描述

二.源码解析

我们打开app目录下的build文件夹,以笔者为例,目录结构为:

app\build\generated\source\apt\debug……\A01SimpleActivity_MembersInjector.java

我们不难发现,编译器已经帮我们生成了这样几个文件:

DaggerA01SimpleComponent.java
Student_Factory.java
A01SimpleActivity_MembersInjector.java

我们进行一一分析:

1. Student_Factory.java

上一篇文章我们已经进行了分析,很简单,当我们@Inject注解一个类的构造方法时,编译器会自动帮我们生成一个工厂类,负责生产该类的对象,类似于商品的厂家

public enum Student_Factory implements Factory<Student> {
  INSTANCE;

  @Override
  public Student get() {
    return new Student();
  }

  public static Factory<Student> create() {
    return INSTANCE;
  }
}

2.DaggerA01SimpleComponent.java

public final class DaggerA01SimpleComponent implements A01SimpleComponent {
  private MembersInjector<A01SimpleActivity> a01SimpleActivityMembersInjector;

  private DaggerA01SimpleComponent(Builder builder) {
    assert builder != null;
    initialize(builder);
  }

  public static Builder builder() {
    return new Builder();
  }

  public static A01SimpleComponent create() {
    return builder().build();
  }

  @SuppressWarnings("unchecked")
  private void initialize(final Builder builder) {
    //初始化A01SimpleActivity_MembersInjector
    this.a01SimpleActivityMembersInjector =
        A01SimpleActivity_MembersInjector.create(Student_Factory.create());
  }

  @Override
  public void inject(A01SimpleActivity activity) {
    a01SimpleActivityMembersInjector.injectMembers(activity);
  }

  public static final class Builder {
    private Builder() {}

    public A01SimpleComponent build() {
      return new DaggerA01SimpleComponent(this);
    }
   /**
     * @deprecated This module is declared, but an instance is not used in the component. This method is a no-op. For more, see https://google.github.io/dagger/unused-modules.
     */
    @Deprecated
    public Builder a01SimpleModule(A01SimpleModule a01SimpleModule) {
      Preconditions.checkNotNull(a01SimpleModule);
      return this;
    }
  }
}

很熟悉,我们在Activity中就用到了这个生成的类,编译器起名方式也很简洁:Dagger+你的Component接口名。

在我们的Activity中我们是这样使用:

DaggerA01SimpleComponent.builder()
//              .a01SimpleModule(new A01SimpleModule(this))
                .build()
                .inject(this);

我们根据这个步骤查看源码,发现

DaggerA01SimpleComponent.builder().build()

实际上是通过建造者模式创建了一个新的DaggerA01SimpleComponent对象,在这个对象的构造方法中,执行了initialize()方法,初始化了一个A01SimpleActivity_MembersInjector对象。

请注意,在初始化A01SimpleActivity_MembersInjector时我们看到这行代码:

A01SimpleActivity_MembersInjector.create(Student_Factory.create());

可以看到,Student工厂类作为参数传入了Injector中。

然后通过调用

DaggerA01SimpleComponent.builder().build().inject(this);

中,实际上是将Activity作为参数传入了A01SimpleActivity_MembersInjector对象的InjectMembers()方法里面,仅此而已。

很好,我们看起来已经明白了Component的作用:编译器通过@Component注解,生成了DaggerA01SimpleComponent类,然后将activity传入初始化了的A01SimpleActivity_MembersInjector对象中。

这时我们有了一点头绪,因为我们发现,Student工厂类,已经和Activity同时都放入了这个神秘的A01SimpleActivity_MembersInjector类中了。

3.A01SimpleActivity_MembersInjector类,将Student和Activity进行连接

public final class A01SimpleActivity_MembersInjector implements MembersInjector<A01SimpleActivity> {
  private final Provider<Student> studentProvider;

  public A01SimpleActivity_MembersInjector(Provider<Student> studentProvider) {
    assert studentProvider != null;
    this.studentProvider = studentProvider;
  }

  public static MembersInjector<A01SimpleActivity> create(Provider<Student> studentProvider) {
    return new A01SimpleActivity_MembersInjector(studentProvider);
  }

  @Override
  public void injectMembers(A01SimpleActivity instance) {
    if (instance == null) {
      throw new NullPointerException("Cannot inject members into a null reference");
    }
    instance.student = studentProvider.get();
  }

  public static void injectStudent(A01SimpleActivity instance, Provider<Student> studentProvider) {
    instance.student = studentProvider.get();
  }
}

其实已经很简单了,在该Injector的injectMembers()方法中,已经将Student对象通过Student_Factory的get()方法获得,然后直接赋值给Activity的student对象了!

就是这行代码:

instance.student = studentProvider.get();

private final Provider studentProvider ->就是在create()方法中传入的Student_Factory工厂类,不信?点击Factory类:

public interface Factory<T> extends Provider<T> {
}

很明显了,Student_Factory父类是 Factory,Factory父类是Provider,向上转型嘛。

三 带Module的源码解析:

现在我们尝试上一篇文章中的Module相关代码:

1.Student类(取消Inject注解):

public class Student {

    public Student() {
    }

}

2.Module类(增加一个Provide注解方法):

@Module
public class A01SimpleModule {

    private A01SimpleActivity activity;

    public A01SimpleModule(A01SimpleActivity activity) {
        this.activity = activity;
    }

    @Provides
    Student provideStudent(){
        return new Student();
    }
}

3.Component(不变)

@Component(modules = A01SimpleModule.class)
public interface A01SimpleComponent {

    void inject(A01SimpleActivity activity);

}

4.Activity(新增一行代码)

public class A01SimpleActivity extends AppCompatActivity {

    @BindView(R.id.btn_01)
    Button btn01;

    @Inject
    Student student;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_a01_simple);
        ButterKnife.bind(this);
        //新增一行代码.a01SimpleModule(new A01SimpleModule(this))
        DaggerA01SimpleComponent.builder()
                .a01SimpleModule(new A01SimpleModule(this))
                .build()
                .inject(this);
    }

    @OnClick(R.id.btn_01)
    public void onViewClicked(View view) {
        switch (view.getId()){
            case R.id.btn_01:
                Toast.makeText(this,student.toString(),Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

我们先把app/build文件夹删除,删除自动生成的代码后,然后ctrl+F9重新编译,编译成功运行,依然可以获得Student对象。

这时我们打开build目录,层层剥开后,发现这样三个类:

DaggerA01SimpleComponent.java
A01SimpleModule_ProvideStudentFactory.java
A01SimpleActivity_MembersInjector.java

1.A01SimpleModule_ProvideStudentFactory.java

public final class A01SimpleModule_ProvideStudentFactory implements Factory<Student> {
  private final A01SimpleModule module;

  public A01SimpleModule_ProvideStudentFactory(A01SimpleModule module) {
    assert module != null;
    this.module = module;
  }

  @Override
  public Student get() {
    return Preconditions.checkNotNull(
        module.provideStudent(), "Cannot return null from a non-@Nullable @Provides method");
  }

  public static Factory<Student> create(A01SimpleModule module) {
    return new A01SimpleModule_ProvideStudentFactory(module);
  }
}

我们知道,我们在Module中创建了一个provideStudent()方法,方法中创建并返回了一个Student对象,其实很相似,Module的@Provides注解就是帮助我们生成了一个Student_Factory的工厂,只不过这个工厂很特别,只有钥匙才能进(必须传入A01SimpleModule对象才能实例化):

//没有传入A01SimpleModule对象,无法实例化该工厂类
 public static Factory<Student> create(A01SimpleModule module) {
    return new A01SimpleModule_ProvideStudentFactory(module);
  }

我们查看A01SimpleModule会发现,想实例化A01SimpleModule,必须传入一个A01SimpleActivity对象,这说明了,A01SimpleModule就像是一个专属的快递箱子,只有本人(A01SimpleActivity)才能签收私人快递,然后打开自己的盒子(A01SimpleModule->创建A01SimpleModule_ProvideStudentFactory)获得这个Student对象!

简单来说,通过@Providers注解后,产生的对象就经过Module包装,通过Component快递员送到需要的容器Activity中。

相比@Inject简单粗暴的注解生成的“万能工厂”Student_Factory类,似乎这个更“安全”一些~

2.DaggerA01SimpleComponent

public final class DaggerA01SimpleComponent implements A01SimpleComponent {
  private Provider<Student> provideStudentProvider;

  private MembersInjector<A01SimpleActivity> a01SimpleActivityMembersInjector;

  private DaggerA01SimpleComponent(Builder builder) {
    assert builder != null;
    initialize(builder);
  }

  public static Builder builder() {
    return new Builder();
  }

  @SuppressWarnings("unchecked")
  private void initialize(final Builder builder) {
    //创建A01Module专属的工厂
    this.provideStudentProvider =
        A01SimpleModule_ProvideStudentFactory.create(builder.a01SimpleModule);
    //将专属工厂放入Injector中
    this.a01SimpleActivityMembersInjector =
        A01SimpleActivity_MembersInjector.create(provideStudentProvider);
  }

  @Override
  public void inject(A01SimpleActivity activity) {
      //将Activity容器传入Injector中
    a01SimpleActivityMembersInjector.injectMembers(activity);
  }

  public static final class Builder {
    private A01SimpleModule a01SimpleModule;

    private Builder() {}

    public A01SimpleComponent build() {
      if (a01SimpleModule == null) {
        throw new IllegalStateException(A01SimpleModule.class.getCanonicalName() + " must be set");
      }
      return new DaggerA01SimpleComponent(this);
    }

    //传入需要的Module
    public Builder a01SimpleModule(A01SimpleModule a01SimpleModule) {
      this.a01SimpleModule = Preconditions.checkNotNull(a01SimpleModule);
      return this;
    }
  }

变化很少,相比较@Inject注解的方式,@Inject注解生成的工厂类就好像将商品赤裸着交给Component,@module注解生成的工厂类就好像给商品加了一层防护纸箱,感觉更贴心了呢~

3.A01SimpleActivity_MembersInjector

public final class A01SimpleActivity_MembersInjector implements MembersInjector<A01SimpleActivity> {
  private final Provider<Student> studentProvider;

  public A01SimpleActivity_MembersInjector(Provider<Student> studentProvider) {
    assert studentProvider != null;
    this.studentProvider = studentProvider;
  }

  public static MembersInjector<A01SimpleActivity> create(Provider<Student> studentProvider) {
    return new A01SimpleActivity_MembersInjector(studentProvider);
  }

  @Override
  public void injectMembers(A01SimpleActivity instance) {
    if (instance == null) {
      throw new NullPointerException("Cannot inject members into a null reference");
    }
    instance.student = studentProvider.get();
  }

  public static void injectStudent(A01SimpleActivity instance, Provider<Student> studentProvider) {
    instance.student = studentProvider.get();
  }
}

可以发现,基本并没有什么变化。

总结

经过两次分析 我们基本理解了Dagger2的使用方式,原理基本如下:

@Inject 注解构造 生成“大众”工厂类
或者
@Module +@Providers 提供注入“私有”工厂类

然后

通过Component 创建获得Activity,获得工厂类Provider,统一交给Injector

最后

Injector将Provider的get()方法提供的对象,注入到Activity容器对应的成员变量中,我们就可以直接使用Activity容器中对应的成员变量了!

了解了原理,接下来怎么使用就随意了~在接下来的文章中,我会结合MVP的架构对Dagger2进行更深入的使用。

GitHub传送门,点我看源码

作者:mq2553299 发表于2017/6/12 21:30:28 原文链接
阅读:9 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live