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

Gradle用户使用指南

$
0
0

转载注明出处:http://blog.csdn.net/xiaohanluo/article/details/72644979

0. 前言

完全由个人翻译,能力有限,有些细节地方翻译不是很通顺,大家可以参考Gradle Plugin User Guide英文版本阅读,如果有问题,欢迎指正。

译文git工程:https://github.com/Kyogirante/gradle_user_guide,欢迎star。

转载请事先沟通,未经允许,谢绝转载。

1. 新工具介绍(Introduction)

  • 能够复用代码和资源
  • 能够构建几种不同版本参数的应用
  • 能够配置、扩展、自定义构建过程

1.1 为什么选择Gradle(Why Gradle?)

Gradle是一款具有优势的构建工具,通过插件可以自定义构建过程。主要优势如下:

  • 基于Groovy的领域特定语言(DSL),用于描述和操作构建过程
  • 支持maven/lvy的依赖管理
  • 非常灵活,并不强迫用户一定要使用最佳的构建方式
  • 插件可以暴露自身的语言和接口api给构建文件使用
  • 支持IDE集成

2.2 需求(Requirements)

  • Gradle 2.2(Gradle版本是2.2及以上,因为文档中有些新特性)
  • SDK with Build Tools 19.0.0.

2. 工程基本配置(Basic Project Setup)

Gradle工程默认的配置文件名称是build.gradle,在主工程的根目录下。

2.1 配置文件示例(Simple build files)

下面是一个Android工程的最简单配置文件的内容。

buildscript {
    repositories { 
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.1'
    }
}

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.1.0"
}

配置内容主要分为三部分。

  • buildscript{},这个部分主要配置在构建过程中的依赖。上面示例中,声明使用jcenter依赖库,声明了一个maven库的依赖com.android.tools.build:gradle:1.3.1,是指引入gradle集成工具,版本是1.3.1。(关于Android Gradle Plugin版本和Gradle版本关系,点这里
  • apply plugin,引用插件,com.android.application这个插件用于构建Android工程
  • android {},这部分是配置构建Android工程的参数。compileSdkVersionbuildToolsVersion是必须的

注意:主工程中只能引用com.android.application插件,如果引用java插件会报错,参考这里,第一个插件说明这是个Android工程,第二个插件说明这是个Java工程,所以只能引用一个。

注意:用户可以在local.properties文件中使用sdk.dir属性配置本地的Android sdk位置,或者设置一个名为Android_HOME的环境变量,这两种方法没有什么区别。

示例local.properties:

sdk.dir=/path/to/Android/Sdk

2.2 工程结构(Project Structure)

Android工程文件有默认的目录结构。Gradle遵循约定由于配置规则,提供合理的默认值。工程以两个目录为主,一个是工程代码目录,一个是测试代码目录。

  • src/main/
  • src/androidTest/

在每个目录中都有一些子目录,Java工程和Android工程共有的子目录如下:

  • java/
  • resources/

Android工程中有一些独有的目录:

  • AndroidManifest.xml
  • res
  • assets
  • aidl
  • rs
  • jni
  • jniLibs

所有的java文件都在src/main/java目录下,主要的配置文件目录是src/main/AndroidManifest.xml

src/main/AndroidManifest.xml是自动创建的,不需要手动创建

2.2.1 配置目录结构(Configuring the Structure)

默认的目录结构并不能完全适配所有情况,用户可以配置目录结构。点击这里查看Java工程师怎么配置目录结构的。

在Android工程中使用同样的格式,但是因为Android工程中有独有的一些目录,所以配置信息需要写在android {}这部分。下面示例中,工程代码使用原来的目录,修改测试代码的目录。

android {
    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            aidl.srcDirs = ['src']
            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
        }

        androidTest.setRoot('tests')
    }
}

注意:因为旧的目录中包含所有的文件(java、AIDL、res等等),所以需要重设所有的目录

注意:setRoot()重设目录位置,沿用之前的目录结构,这个是Android工程特有的

2.3 构建任务(Build Tasks)

2.3.1 通用任务(General Tasks)

使用插件(包含Java和Android插件)去构建工程会自动创建很多任务,通用的任务如下:

  • assemble,打包工程所产出的文件
  • check,运营工程中所有的check任务
  • build, 执行assemble任务和check任务
  • clean,清除工程的产出的文件

assemblecheckbuild这三个任务实际上并不做任何事,他们只是一个壳任务,实告诉Gradle去执行那些的任务。

不管什么工程,依赖了什么插件,都可以反复去调用同一个任务。例如引用一个findBugs插件,会创建一个新的任务,让check任务依赖这个新任务,这样每次调用check任务时候,新建的任务也会执行。

  • 使用gradle tasks指令获取工程中所有的可执行任务
  • 使用gradle tasks --all执行获取工程中所有可执行任务简介以及依赖关系

如果工程中未做任何修改,执行build任务,每个任务描述后面都会加上UP-TO-DATE,这意味着这个任务不需要真正地执行,因为工程没有改动。这样每个任务都可以依赖其他任务,而且不需要其他任务做构建工作。

2.3.2 Java工程任务(Java project tasks)

引用Java插件时候,说明这个工程是个纯Java工程,会额外添加两个壳任务jartests

  • assemble
    • jar 打包工程产出文件
  • check
    • tests 执行所有测试

jar任务会直接或者间接的依赖任务classes,这个任务会编译java源代码;tests任务会依赖任务testClasses,但是很少会直接调用这个任务,因为tests任务依赖它,直接调用tests任务即可。

大体上,用户可能只会调用assemblecheck任务,很少调用其他任务。可以点击这里查看Java工程所有的任务和任务描述。

2.3.3 Android工程任务(Android tasks)

引用com.android.application插件,说明这个工程是Android工程,在通用任务基础上会额外添加两个壳任务。

  • connectedCheck,查看是否有设备连接
  • deviceCheck, 查看是否连接上设备

注意,build任务是不依赖connectedCheckdeviceCheck任务的。

一个Android工程至少有两个构建包,debug apk和release apk。每一个构建包都有自己的壳任务。

  • assemble
    • assembleDebug
    • assembleRelease

这两个任务会依赖其他一些任务,要构建出一个安装包,需要执行好多步骤。assemble任务依赖这两个任务,所以执行assemble任务时候,会产出debug和release两个apk。

注意:Gradle支持指令简写模式,例如gradle aRgradle assembleRelease意义是相同的,只需要保证没有其他任务能简写成aR

Android工程中check类任务有各自的依赖。

  • check
    • lint
  • connectedCheck
    • connectedAndroidTest
  • deviceCheck
    • 它依赖于那些扩展了tests通用任务的任务

最后,Android工程中,也会有对程序安装和卸载的任务。

  • installDebug
  • installRelease
  • uninstallAll
    • uninstallDebug
    • uninstallRelease
    • uninstallDebugAndroidTest

2.4 自定义基本构建(Basic Build Customization)

Android的插件提供了领域特定语言(DSL)来帮助用户直接地自定义构建过程。

2.4.1 清单内容(Manifest entries)

通过DSL用户可以设置一些构建参数,可设置内容如下:

  • minSdkVersion
  • targetSdkVersion
  • versionCode
  • versionName
  • applicationId (最终有效的包名,点击这里查看细节)
  • testApplicationId (用于测试app)
  • testInstrumentationRunner

示例如下:

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"


    defaultConfig { 
        versionCode 12
        versionName "2.0"
        minSdkVersion 16
        targetSdkVersion 23
    }
}

点击这里查看可以配置的清单参数信息。

可以在.gradle文件中动态配置这些清单信息,例如,动态配置versionName参数,示例如下:

def computeVersionName() {
    ...
}


android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"


    defaultConfig {
        versionCode 12 
        versionName computeVersionName()
        minSdkVersion 16
        targetSdkVersion 23
    }
}

注意:自定义时尽量不要使用gettter的方法名,防止冲突,例如,defaultConfig.getVersionName()会替换掉自定义的getVersionName()方法,也就是说每一个参数都有默认的getter方法

2.4.2 构建类型(Build Types)

Android工程中默认的会有debug和release两种构建方式,主要区别在于调试程序的能力以及apk签名细节。debug的版本为了防止在构建过程中弹出提示,系统会根据明文的用户名/密码自动创建一个数字证书用于签名,使用debug证书签名的apk是无法上架销售的。release版本在构建过程中不进行签名,将签名放在之后的环节。

buildTypes中配置构建类型信息,默认会创建两种构建方式,debug和release,在Android工程中允许自定义这两种构建方式的具体细节信息。示例如下:

android {
    buildTypes {
        debug {
            applicationIdSuffix ".debug"
        }


        jnidebug {
            initWith(buildTypes.debug)
            applicationIdSuffix ".jnidebug"
            jniDebuggable true
        }
    }
}

上面代码作用:

  • 设置debug构建类型的包名是<app appliationId>.debug,这样一台设备上面就可以同时安装debug和release的包,不会出现包名冲突情况
  • 新建一个新的构建类型,名为jnidebuginitWith(buildTypes.debug)表示buildTypes.debug构建类型(Build Type)配置信息应用到这个构建中
  • 重新设置包名同时设为jniDebuggable为true,开启debug模式

buildTypes中新建一个新的构建类型非常方便,可以使用initWith()复用其他构建类型的构建参数。点击这个查看可配置的构建参数。

除了修改构建参数意外,buildTypes中还可以添加特定的代码和资源。每一种构建类型,默认都有一个特定资源目录src/<build_type_name>/,例如src/debug/java目录,只有构建debug类型apk时候才会用到这个目录下的资源。这就意味着构建类型不能是mainandroidTest,这两个目录是工程的默认目录,参考上面2.2提到的目录结构。

跟上文提到的sourceSet一样,每一种构建类型可以重设目录,示例如下:

android {
    sourceSets.jnidebug.setRoot('foo/jnidebug')
}

另外,对于每一个构建类型,都会有一个新的工程任务被创建,名为assemble<Build Type Name>,例如上文提到的assembleDebugassembleRelease, 这两个任务也是来源于此。

根据这个规则,上面配置信息就会产生assembleJnidebug新任务,assemble任务像依赖assembleDebugassembleRelease任务一样,也会依赖这个新任务。

可能新建构建类型的场景:

  • 某些权限/模式在debug才开启,release版本不开启
  • 自定义debug调试实现
  • debug模式需要一些额外的资源

构建中设置的代码/资源主要用于以下几点:

  • 合并到主清单
  • 代码实现替换
  • 资源覆盖

2.4.3 签名信息配置(Signing Configurations)

应用签名以下信息是必须的:

  • A keystore
  • A keystore password
  • A key alias name
  • A key password
  • The store type

点击这里查看Android官方签名细节及具体过程。

点击这里查看可配置的签名信息,这些信息是在signingConfigs{}模块中配置的。

Android工程中,debug构建会用通用的debug.keysotre和密码、通用的key和密码,keystore文件位于$HOME/.android/debug.keystore这个目录。

具体示例如下:

android {
    signingConfigs {
        debug {
            storeFile file("debug.keystore")
        }


        myConfig {
            storeFile file("other.keystore")
            storePassword "android"
            keyAlias "androiddebugkey"
            keyPassword "android"
        }
    }


    buildTypes {
        foo {
            signingConfig signingConfigs.myConfig
        }
    }
}

上述示例中声明了两个签名类型signingConfigs.debugsigningConfigs.myConfig,两者都将设置了keystore位置,位于工程根目录,同时
myCondig设置了其他必需信息,debug使用通用信息,不用配置。

注意:只有debug类型签名的keystore位于默认位置,系统才会自动创建,如果重设了keystore的位置,就不会自动创建。新建签名类型会自动使用默认的keystore,如果没有,系统会自动创建,也就是说,系统是否自动创建keystore,是跟签名类型的storeFile的位置有关系,跟签名类型的名称没有关系。

注意:storeFile所以的目录在工程的根目录,是个相对目录,当然也可以设置为绝对目录,但是不推荐这样做

注意:如果要根据具体情况来控制签名参数,就不能直接将key和密码等信息直接写在signingConfigs中,可以在gradle.properties文件中设置签名具体细节,然后在signingConfigs引用,具体点击这里查看

3. 工程依赖/Android库/多工程设置(Dependencies, Android Libraries and Multi-project setup)

Gradle工程可以依赖其他组件,这些组件可能是外部jar包也可能是一个Gradle工程。

3.1 依赖jar包(Dependencies on binary packages)

3.1.1 本库jar包(Local packages)

依赖外部jar包,需要在.gradle文件中使用compile进行配置,示例如下:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}


android {
    ...
}

注意:dependencies属于标准Gradle API的DSL属性,并不是属于andorid{}的元素

compile属性用于编译整个工程,compile的库都会被添加到编译路径中,最终也会打包到最终的apk中。依赖类型分为以下几种:

  • compile,主工程
  • androidTestCompile,测试工程
  • debugCompile, debug构建类型
  • releaseCompile, release构建类型

因为构建一个apk不可能没有构建类型,所以一般至少有两个compile类型配置(compile<buildtype>Compile)甚至更多。每创建一个新的构建类型,系统都会自动基于构建类型的名称创建一个新的compile类型,名为<buildtype>Compile。这在构建打包过程非常有用,例如debug版本需要某个外部库而release版本不需要,又比如不同的构建打包对同一个外部库依赖的版本不同。点击这里查看解决jar包版本冲突的具体信息。

3.1.2 远程依赖(Remote artifacts)

Gradle支持从Maven/Lvy仓库拉取依赖。首先声明仓库,然后要在声明具体的依赖。示例如下:

repositories {
     jcenter()
}


dependencies {
    compile 'com.google.guava:guava:18.0'
}


android {
    ...
}

注意:jcenter()是一个仓库URL的缩写。Gradle支持远端和本地仓库。

注意:Gradle支持依赖传递,也就是说A工程依赖B,B依赖C,那么A工程也会依赖C,A工程会从仓库中获取C。

点击这里查看更多的依赖设置细节;点击这里查看具体依赖设置语言示例。

3.2 多工程设置(Multi project setup)

通过多工程依赖设置,Gradle工程可以依赖其他的Gradle工程。多工程设置是通过将其他被依赖的Gradle工程放入主工程的子目录中,如下结构:

MyProject/
 + app/
 + libraries/
    + lib1/
    + lib2/

这里面有三个Gradle工程,通过以下的方式能够引用到具体的工程:

  • :app
  • :libraries:lib1
  • :libraries:lib2

每一个工程拥有自己的build.gradle文件,配置该工程的构建细节,目录结构如下:

MyProject/
 | settings.gradle
 + app/
    | build.gradle
 + libraries/
    + lib1/
       | build.gradle
    + lib2/
       | build.gradle

另外,在上面结构中可以看见在主工程的根目录中有一个settings.gradle文件,这个文件是用来定义那些目录是Gradle工程,settings.gradle示例内容如下,里面定义了三个Gradle工程目录:

include ':app', ':libraries:lib1', ':libraries:lib2'

主工程:app想要依赖其他的Gradle工程,只需要在它自身的build.gradle文件中添加依赖关系:

dependencies {
     compile project(':libraries:lib1')
}

android {
      ...
}

点击这里查看更多多工程设置细节。

3.3 库工程(Library projects)

上面所提到的多工程设置,:libraries:lib1, :libraries:lib2可以是Java工程,:app使用它们产生的jar包。但是,如果你想共享那些使用Android APIs或者使用Android-style的资源文件的代码,就不能使用上述的普通的Java工程,必须是Android库工程。

3.3.1 创建库工程(Creating a Library Project)

库工程和平常的Android工程很类似,有一些细小的区别。构建库工程(Library)和构建一个应用工程(Application)是不同的,所以需要引用另一个插件’com.android.library’,和com.android.application插件一样,都是由com.android.tools.build.gradlejar包提供。下面是库工程build.gradle文件的示例。

buildscript {
    repositories {
        jcenter()
    }


    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.1'
    }
}


apply plugin: 'com.android.library'


android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"
}

这个库工程使用sdk编译版本是23,SourceSetbuildTypesdependencies都沿用他们所在的主工程,当然,也可以在库工程自定义这些构建信息。

3.3.2 库工程和应用工程(主工程)的区别(Differences between a Project and a Library Project)

库工程主要产出一个代表Android库的aar包,里面包括编译后的代码(jar包和.so文件)和一些资源文件(manifest、res、assets);库工程也可以构建出一个测试apk用于测试库工程,这个测试apk是独立于主工程apk的,库工程有assembleDebugassembleRelease壳任务,所以用指令构建库工程和构建主工程是没有区别的。其余地方,库功臣和主工程是相同的。他们都有构建类型buildTypes和定制版本product flavors(后续会讲解),可以产出多个版本的aar包。注意buildTypes中大多数构建参数不适用于库工程,同时,可以通过更改sourceSet更改库工程的内容,这取决于库工程是被主工程使用,还是用于测试。

3.3.3 引用库工程(Referencing a Library)

引用库工程示例如下:

dependencies {
    compile project(':libraries:lib1')
    compile project(':libraries:lib2')
}

android {
      ...
}

3.3.4 发布库工程(Library Publication)

库工程会默认发布release版本,这个版本可以被其他所有的工程引用,与这些工程的构建版本无关。可以通过设置参数控制库工程发布的版本,示例如下:

android {
    defaultPublishConfig "debug"
}

注意defaultPublishConfig内容是构建版本全名,releasedebug是系统默认的构建版本名,我们也可以改成我们自定义的构建版本全名,示例如下:

android {
    defaultPublishConfig "flavor1Debug"
}

当然,也可以发布所有版本的库工程,示例如下:

android {
    publishNonDefault true
}

发布多个库工程版本意味着会产生多个aar包,而不是一个aar包包含多个版本,每个aar包都是一个独立的版本。

不同的构建可以依赖同一个库工程的不同版本,示例如下:

dependencies {
    flavor1Compile project(path: ':lib1', configuration: 'flavor1Release')
    flavor2Compile project(path: ':lib1', configuration: 'flavor2Release')
}

注意:发布版本的defaultPublishConfig变量的内容必须是构建版本的完整名

注意:一旦设置了publishNonDefault true,会将所有版本的aar包都上传到统一maven仓库,但是,这种做法是不合理的,一个maven仓库目录应该仅对应一个系列版本的aar包,例如debugrelease版本的aar包分别在不同的maven仓库目录中,或者保证不同版本的依赖仅仅发生在工程内部,不上传到maven仓库。

4. 测试(Testing)

可以建立一个测试工程集成到主工程当中,不需要单独新建一个测试工程。

4.1 单元测试(Unit testing)

Gradle 1.1版本之后就支持单元测试,点击这里查看详情。本章所提及的真机测试instrumentation tests,是指需要单独构建一个测试apk,运行在真机或者模拟器上的一种测试。

4.2 基本配置(Basics and Configuration)

上文中提到Android工程中默认有两个目录src/main/src/androidTest/。使用src/androidTest/这个目录中的资源会构建一个使用Android测试框架,并且布署到真机(或测试机)上的测试apk来测试应用程序。Android测试框架包含单元测试、真机测试、UI自动化测试。测试apk的清单配置中的<instrumentation>节点会自动生成,同时用户也可以在src/androidTest/AndroidManifest.xml中添加额外模块用于测试。

下面列出测试apk中可能用到的属性,点击这里查看详情:

  • testApplicationId
  • testInstrumentationRunner
  • testHandleProfiling
  • testFunctionalTest

这些属性是在andorid.defaultConfig中配置的,示例如下:

android {
    defaultConfig {
        testApplicationId "com.test.foo"
        testInstrumentationRunner "android.test.InstrumentationTestRunner"
        testHandleProfiling true 
        testFunctionalTest true
    }
}

在测试程序的清单配置(manifest)中,<instrumentation>节点中的targetPackage属性会根据被测试的应用程式包名自动生成,这个属性不受自定义的defaultConfig配置和buildType配置所影响。这也是manifest文件需要自动生成的一个原因。

另外,androidTest可以有自己的依赖配置,默认情况下,应用程序和它的依赖都会自动添加到测试应用的classpath中,也可以通过手动拓展测试的依赖,示例如下:

dependencies {
    androidTestCompile 'com.google.guava:guava:11.0.2'
}

使用assembleAndroidTest任务来构建测试apk,这个任务不依赖于主工程的assemble任务,当设置要执行测试时候,这个任务会自动执行。

默认只有一个buildType会被测试,debug的构建类型,但是可以自定义修改被测试的buildType,示例如下:

android {
    ...
    testBuildType "staging"
}

4.3 解决冲突(Resolving conflicts between main and test APK)

当启动真机测试的时候,主apk和测试apk会共享同一个classpath,一旦两个apk使用了同一个库,但是使用的是不同版本,gralde构建就会失败。如果Gradle没有捕获这种情况,应用程式在测试和实际使用中可能表现不同(崩溃只是其中一种表现)。

为了促使构建成功,只需要让所有的apk使用同一个版本的库。如果这个冲突是发生在简介依赖中(没有直接在build.gradle中引入的库),只需要在build.gradle中引入这个库最新的版本即可,使用compile或者androidTestCompile。详情查看这里Gradle’s resolution strategy mechanism。可以通过./gradlew :app:dependencies and ./gradlew :app:androidDependencies查看工程的依赖树。

4.4 执行测试(Running tests)

上文中提到check类的任务(需要连接设备)是通过connectedCheck壳任务被唤起的,这个过程依赖connectedDebugAndroidTest任务,因此执行测试,需要执行connectedDebugAndroidTest任务,它会有以下操作:

  • 确认主应用和测试应用都被构建(依赖assembleDebugassembleDebugAndroidTest任务)
  • 安装主应用和测试应用
  • 运行测试
  • 卸载主应用和测试应用

如果有多个设备连接,所有的测试会并行在所有设备上运行,其中任何一个设备测试失败,测试就失败。

4.5 测试Andorid库(Testing Android Libraries)

测试Andriod库和测试Android程序是相同的。不同的是Android库作为依赖直接继承到测试应用中,这样测试apk不仅包含测试的代码还包含这个库以及这个库的依赖。Android库的清单配置会合并到测试程序的清单配置中。androidTest任务改为只安装测试应用(没有其他应用),其他都是相同的。

4.6 测试报告(Test reports)

当执行单元测试后,Gradle会生成一份HTML报告方便查看测试结果。Andorid插件是在此基础上扩展了HTML报告,聚合了所有连接设备的测试结果。所有的测试结果以XML形式储存在build/reports/androidTests/目录下,这个目录也是可配的,示例如下:

android {
    ...

    testOptions {
        resultsDir = "${project.buildDir}/foo/results"
    }
}

4.6.1 多工程测试报告(Multi-projects reports)

在配置了多工程或者多依赖的工程中,当同时运行所有测试时候,针对所有的测试只生成一份测试报告是非常有用的。

为了到达这个目的,需要使用另一个插件,这个插件是Android插件中自带的,示例如下:

buildscript {
    repositories {
        jcenter()
    }


    dependencies {
        classpath 'com.android.tools.build:gradle:0.5.6'
    }
}


apply plugin: 'android-reporting'

这必须添加到工程的根目录下,例如和settings.gradle同目录的build.gralde中,然后在根目录中使用使用一下指令运行所有测试,同时合并所有测试报告:

gradle deviceCheck mergeAndroidReports --continue

注意:--continue是为了保证所有测试都执行,即使其中子项目中任何一个测试失败。如果没有这个选项,当有测试失败时候,整个测试过程就会中断。

4.7 Lint支持(Lint support)

注:lint是一种检查Android项目的工具

可以针对某一个构建版本执行lint,例如, ./gradlew lintRelease,或者针对所有版本./gradlew lint,lint会生成一个记录被检查版本问题的报告。可以通过配置lintOption来设置lint细节,点击这里查看详情,示例如下:

android {
    lintOptions {
        // turn off checking the given issue id's
        disable 'TypographyFractions','TypographyQuotes'

        // turn on the given issue id's
        enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'

        // check *only* the given issue id's
        check 'NewApi', 'InlinedApi'
    }
}

5. 构建版本(Build Variants)

使用新构建工具的目的之一是面对同一个工程,能够编译出不同的版本。

有两个场景会用到:

  • 同一个应用有多个版本,例如,免费版本和付费版本
  • 同一个应用针对不同的设备有多个版本,比如说手机版和pad版,点击这里查看详情
  • 1和2综合

针对同一个工程,可以编译出不同版本的apks,而不是为了编译出不同版本的apks,使用多个工程。

5.1 Product flavors

product flavor可自定义应用的版本,同一个工程中可以有多个不同的product flavor

product flavor用来告诉构建系统不同版本之间的细小区别,product flavor的声明示例如下:

android {
    ....

    productFlavors {
        flavor1 {
            ...
        }

        flavor2 {
            ...
        }
    }
}

里面创建了两个product flavor,分别是flavor1flavor2

注意:product flavor的名称不能和构建类型(buildType)的名称相同,也不能是androidTesttest

5.2 Build Type + Product Flavor = Build Variant

构建类型 + 定制版本 = 构建版本(应用版本)

就像上文提到的,每一个构建类型(buildType)都可以构建一个apk,同样的,每一个定制版本(productFlavor)都可以构建一个apk,这样的话,构建类型(buildType)和定制版本(productFlavor)结合就会形成一个新的apk,也就是构建版本。默认有两种构建类型debugrelease,再加上上文定义的flavor1flavor2,就会形成四种组合,代表四种不同的构建版本:

  • Flavor1 - debug
  • Flavor1 - release
  • Flavor2 - debug
  • Flavor2 - release

一个有没有定义productFlavors的工程也有构建版本,使用缺省的productFlavors,也就是没有product flavor名称,那么工程所有的构建版本和构建类型是一样的。

5.3 配置定制版本(Product Flavor Configuration)

productFlavors配置示例如下:

android {
    ...

    defaultConfig {
        minSdkVersion 8
        versionCode 10
    }

    productFlavors {
        flavor1 {
            applicationId "com.example.flavor1"
            versionCode 20
         }

         flavor2 {
             applicationId "com.example.flavor2"
             minSdkVersion 14
         }
    }
}

注意,android.productFlavors.*android.defaultConfig中可配置的参数是相同的,也就是说他们共享这些参数。

defaultConfig为所有productFlavor提供一些基本配置参数,每一个productFlavors自定义一些额外的配置参数,也可以覆盖defaultConfig中配置的参数。在上面的示例中,productFlavors配置信息如下:

  • flavor1
    • applicationId: com.example.flavor1
    • minSdkVersion: 8
    • versionCode: 20
  • flavor2
    • applicationId: com.example.flavor2
    • minSdkVersion: 14
    • versionCode: 10

通常,构建类型(buildType)也会修改一些配置信息,比如说buildType中的applicationIdSuffix变量拼接在productFlavorapplicationId之后的。但是有些配置参数是以productFlavor为主的,比如signingConfig,所有的release版本的构建都会使用android.buildTypes.release.signingConfig中配置的签名信息,如果设置了productFlavor,所有的release版本的构建都会使用android.productFlavors.*.signingConfig中配置的签名信息。

5.4 资源目录和依赖(Sourcesets and Dependencies)

和构建类型相似,productFlavor也有自己的资源目录,示例如下:

  • android.sourceSets.flavor1,资源目录是src/flavor1/
  • android.sourceSets.flavor2,资源目录是src/flavor2/
  • android.sourceSets.androidTestFlavor1,资源目录是src/androidTestFlavor1/
  • android.sourceSets.androidTestFlavor2,资源目录是src/androidTestFlavor2/

这些资源目录中的资源会用于构建apk,构建apk资源的来源是android.sourceSets.main主工程的资源和构建类型的资源目录(或者productFlavor的资源目录)。下面是多个资源目录构建规则:

  • 所有的资源代码(src/*/java)最终都会合并到最后输出包中
  • 所有的Manifests.xml也会合并,根据buildTypesproductFlavor,每个不同的构建包apk,会有不同的组件和权限
  • 所有的资源(包括res和assets)都会做合并,资源会做合并,资源优先级 buildType > productFlavor > 主工程
  • 每一个构建版本都有唯一的一个R.class,不和其他构建版本共享

最后,和构建类型(buildType)一样,每一个productFlavor都有自己的依赖。例如flavor1需要依赖广告组件和支付组件,而flavor2仅仅依赖广告组件,配置文件如下:

dependencies {
    flavor1Compile "ads.sdk"
    flavor1Compile "pay.sdk"

    flavor2Compile "ads.sdk"
}

另外每一个构建版本都有一个资源目录,示例如下:

  • android.sourceSets.flavor1Debug,资源目录src/flavor1Debug/
  • android.sourceSets.flavor1Release,资源目录src/flavor1Release/
  • android.sourceSets.flavor2Debug,资源目录src/flavor2Debug/
  • android.sourceSets.flavor2Release,资源目录src/flavor2Release/

构建版本目录资源的优先级高于构建类型资源目录。

现在基本知道,buildTypesproductFlavorbuildVariants都有自己的资源目录,资源优先级是:

buildVariants > buildType > productFlavor > 主工程。

5.5 构建任务(Building and Tasks)

上文中提到,每新建一种构建类型buildType,都会自动创建一个名为assemble<Build Type Name>的新任务。

而每新建一种productFlavor,会自动创建多个新任务:

  1. assemble<Variant Name>,直接构建一个最终版本的apk,例如assembleFlavor1Debug任务
  2. assemble<Build Type Name>,构建所有buildType类型的apks,例如,assembleDebug任务会构建出Flavor1DebugFlavor2Debug版本的apk
  3. assemble<Product Flavor Name>,构建所有productFlavor的apks,例如,assembleFlavor1任务会构建出Flavor1DebugFlavor1Release版本的apk.

assemble会构建所有版本的apk。

5.6 多flavor构建(Multi-flavor variants)

注:原文中dimension of Product Flavors,统一翻译为productFlavor类型,在某些文档中也翻译成维度

在某些场景下,一个应用可能需要基于多个标准创建多个版本。例如,Google Play的multi-apk支持四个不同的过滤器,这些用于创建不同apk的过滤器需要使用多个类型的ProductFlavor

例如,一个游戏有免费版本和付费版本,同时在multi-apk中需要支持ABI过滤器(ABI,二进制接口,可以让编译好的目标代码在所有支持该ABI的系统上运行,而无需对程序进行修改)。一个拥有两个版本和三个ABI过滤器的工程,需要创建六个apks(不考虑构建类型buildType),但是它们使用的源代码都是相同的,所以没有必要创建六个productFlavor。相反,只需要创建两个类型的flavor,就可以构建出所有的可能的版本组合。

使用flavorDimensions数组来实现多个类型的flavor,每一个productFlavor被分到不同的类型,示例如下:

android {
    ...


    flavorDimensions "abi", "version"


    productFlavors {
        freeapp {
            dimension "version"
            ...
        }

        paidapp {
            dimension "version"
            ...
        }


        arm {
            dimension "abi"
            ...
        }

        mips {
            dimension "abi"
            ...
        }

        x86 {
            dimension "abi"
            ...
        }
    }
}

android.flavorDimensions数组按顺序定义了可能使用到的flavor类型,每一个productFlavor声明自身的flavor类型。

上面例子中,将productFlavor分为两个类型,abi类型[arm, mips, x86]和version类型[freeapp, paidapp],加上默认的[debug, release]构建类型,将会组合出以下这些构建版本(Build Variant):

  • x86-freeapp-debug
  • x86-freeapp-release
  • arm-freeapp-debug
  • arm-freeapp-release
  • mips-freeapp-debug
  • mips-freeapp-release
  • x86-paidapp-debug
  • x86-paidapp-release
  • arm-paidapp-debug
  • arm-paidapp-release
  • mips-paidapp-debug
  • mips-paidapp-release

android.flavorDimensions数组定义的flavor类型顺序非常重要。

上述每一个构建版本名称都由以下几个属性构成:

  • android.defaultConfig
  • abi类型
  • version类型

flavor工程也有自身的资源目录,和构建版本目录相似但是目录名称不包含构建类型,例如:

  • android.sourceSets.x86Freeapp,资源目录是src/x86Freeapp/
  • android.sourceSets.armPaidapp,资源目录是src/armPaidapp/

flavor的资源目录优先级高于productFlavor资源目录,但是低于构建类型资源目录优先级。

那么就可以列出整个工程资源优先级,资源优先级是:

buildVariants > buildType > 多flavor > productFlavor > 主工程

5.7 测试(Testing)

测试多flavor项目和测试一般项目类似。

androidTest的目录适用于所有flavor的测试,每一个flavor也有单独的测试资源目录,例如:

  • android.sourceSets.androidTestFlavor1,资源目录src/androidTestFlavor1/
  • android.sourceSets.androidTestFlavor2,资源目录src/androidTestFlavor2/

类似的,每个flavor也有自己的依赖配置,示例如下:

dependencies {
    androidTestFlavor1Compile "..."
}

通过deviceCheck任务或者主工程的androidTest任务会执行androidTestFlavor1Compile任务。

每个flavor也有自己任务用于执行测试,androidTest<VariantName>,例如:

  • androidTestFlavor1Debug
  • androidTestFlavor2Debug

类似的,测试apk的构建、安装、卸载任务:

  • assembleFlavor1Test
  • installFlavor1Debug
  • installFlavor1Test
  • uninstallFlavor1Debug

最终,会根据flavor生成HTML测试报告,也会生成集成测试报告。测试报告的目录示例如下:

  • build/androidTest-results/flavors/,单个flavor测试报告的目录
  • build/androidTest-results/all/,合并flavor的测试报告
  • build/reports/androidTests/flavors,单个flavor测试报告的
  • build/reports/androidTests/all/,合并flavor的测试报告

即使自定义目录,也只会改变根目录,里面的具体子目录不会改变。

5.8 构建配置(BuildConfig)

在编译时,Android Studio会生成一个类BuildConfig,这个类包含构建特定版本时用到的一些常量,用户可以根据这些常量执行不同的操作行为。例如:

private void javaCode() {
    if (BuildConfig.FLAVOR.equals("paidapp")) {
        doIt();
    else {
        showOnlyInPaidAppDialog();
    }
}

下面是BuildConfig类包含的一些常量:

  • boolean DEBUG – if the build is debuggable.
  • int VERSION_CODE
  • String VERSION_NAME
  • String APPLICATION_ID
  • String BUILD_TYPE – 构建类型,例如: “release”
  • String FLAVOR – productFlavor名称,例如: “paidapp”

如果工程中使用了flavorDimensions多类型flavor,会自动生成额外的变量。以上述的配置文件为例:

  • String FLAVOR = “armFreeapp”
  • String FLAVOR_abi = “arm”
  • String FLAVOR_version = “freeapp”

5.9 过滤构建版本(Filtering Variants)

当添加productFlavor或者使用flavorDimensions设置多类型flavor,可能有些构建版本并不需要。例如,用户定义了两个productFlavor,一个是正常版本,另一个仿造数据用于测试。第二个productFlavor仅仅是在开发过程中有用,在构建发布包时不需要这个productFlavor,可以通过使用variantFilter过滤器移除不需要的构建版本。示例如下:

android {
    productFlavors {
        realData
        fakeData
    }

    variantFilter { variant ->
        def names = variant.flavors*.name

        if (names.contains("fakeData") && variant.buildType.name == "release") {
            variant.ignore = true
        }
    }
}

使用以上配置后,工程就只有以下的构建版本:

  • realDataDebug
  • realDataRelease
  • fakeDataDebug

点击这里查看可以过滤的构建属性。

6. 构建定制进阶(Advanced Build Customization)

6.1 混淆(Running ProGuard)

ProGuard插件是Android插件中自带的,如果构建任务(Build Type)中通过设置minifyEnabled为true(意为使用混淆),混淆任务会自动创建。示例如下:

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFile getDefaultProguardFile('proguard-android.txt')
        }
    }

    productFlavors {
        flavor1 {
        }
        flavor2 {
            proguardFile 'some-other-rules.txt'
        }
    }
}

构建版本同时使用构建类型(Build Type)和productFlavor中设置的混淆规则文件。

默认有两个混淆规则文件:

  • proguard-android.txt
  • proguard-android-optimize.txt

它们位于Android SDK中,使用getDefaultProguardFile(fileName)获取它们的绝对路径名,差别是第二个文件会启用优化。

6.2 忽略资源(Shrinking Resources)

这个配置设置为true,在构建打包时候,会自动忽略没有被使用到的文件。点击这里查看详细信息。示例如下:

android {
 buildTypes {
        release {
            shrinkResources true
              ...
        }
    }
}

6.3 操作任务(Manipulating tasks)

Java工程使用固定的任务一起协作最终打包工程。其中classes任务是用来编译Java源代码的,可以在build.gradle使用classes,它是project.tasks.classes的缩写。

在Android工程中,如果要构建打包,可能会复杂一些,因为Android工程中有大量名字相同的任务,而且它们的名字是基于buildTypeproductFlavor生成的。

为了解决这个问题,Android对象有两个属性:

  • applicationVariants,只适用于com.android.application插件
  • libraryVariants,只适用于com.android.library插件
  • testVariants,两种插件都适用

这三个属性分别返回一个ApplicationVariant、LibraryVariant和TestVariant对象的DomainObjectCollection

注意,适用这三个collection中的任意一个,都会生成所有相对应的任务,也就是说使用collection后,就不需要再更改配置。

DomainObjectCollection可以直接访问所有对象,或者通过过滤器进行筛选。

android.applicationVariants.all { variant ->
   ....
}

这三个Variant类共享下面这些属性:

属性名 属性类型 描述
name String BuildVariant名称,必须保证唯一
description String BuildVariant的描述说明
dirName String BuildVariant的子文件名,必须是唯一的可能会有多个,例如:debug/flavor1
baseName String BuildVariant构建包的基础名字,必须唯一
outputFile File BuildVariant的输出文件,是个可读写的属性
processManifest ProcessManifest 处理清单Manifest的任务
aidlCompile AidlCompile 编译AIDL文件的任务
renderscriptCompile RenderscriptCompile 处理Renderscript文件的任务
mergeResources MergeResources 合并资源的任务
mergeAssets MergeAssets 合并assets资源的任务
processResources ProcessAndroidResources 处理和编译资源文件的任务
generateBuildConfig GenerateBuildConfig 生成BuildConfig的任务
javaCompile JavaCompile 编译Java源代码的任务
processJavaResources Copy 处理Java资源的任务
assemble DefaultTask BuildVariant构建壳任务

ApplicationVariant类还有以下附加属性:

属性名 属性类型 描述
buildType BuildType BuildVariant的构建类型
productFlavors List BuildVariant的productFlavor,不会为null但可以为空
mergedFlavor ProductFlavor 合并android.defaultConfigvariant.productFlavors的任务
signingConfig SigningConfig BuildVariant使用的签名
isSigningReady boolean true表示BuildVariant配置了签名所需要的信息
testVariant BuildVariant 用于测试这个BuildVariant的BuildVariant
dex Dex 将代码打包成dex的任务,如果是库工程,那么这个任务不能为null
packageApplication PackageApplication 打包最终apk的任务,如果是个库工程,这个任务可以为null
zipAlign ZipAlign zip压缩apk的任务,如果是个库工程或者apk不被签名,这个任务可以为null
install DefaultTask 安装apk任务,不能为null
uninstall DefaultTask 卸载apk任务

LibraryVariant类有以下附加属性:

属性名 属性类型 描述
buildType BuildType BuildVariant的构建类型
mergedFlavor ProductFlavor The defaultConfig values
testVariant BuildVariant 用于测试这个BuildVariant的BuildVariant
packageLibrary Zip 用于打包aar的任务,如果是库工程,不能为null

TestVariant类有以下附加属性:

属性名 属性类型 描述
buildType BuildType BuildVariant的构建类型
productFlavors List BuildVariant的productFlavor,不会为null但可以为空
mergedFlavor ProductFlavor 合并android.defaultConfigvariant.productFlavors的任务
signingConfig SigningConfig BuildVariant使用的签名
isSigningReady boolean true表示BuildVariant配置了签名所需要的信息
testedVariant BaseVariant 被TestVariant测试的BaseVariant
dex Dex 将代码打包成dex的任务,如果是库工程,那么这个任务不能为null
packageApplication PackageApplication 打包最终apk的任务,如果是个库工程,这个任务可以为null
zipAlign ZipAlign zip压缩apk的任务,如果是个库工程或者apk不被签名,这个任务可以为null
install DefaultTask 安装apk任务,不能为null
uninstall DefaultTask 卸载apk任务
connectedAndroidTest DefaultTask 在连接设备上执行Android测试的任务
providerAndroidTest DefaultTask 使用拓展API执行Android测试的任务

Android特有任务类型的API:

  • ProcessManifest
    • File manifestOutputFile
  • AidlCompile
    • File sourceOutputDir
  • RenderscriptCompile
    • File sourceOutputDir
    • File resOutputDir
  • MergeResources
    • File outputDir
  • MergeAssets
    • File outputDir
  • ProcessAndroidResources
    • File manifestFile
    • File resDir
    • File assetsDir
    • File sourceOutputDir
    • File textSymbolOutputDir
    • File packageOutputFile
    • File proguardOutputFile
  • GenerateBuildConfig
    • File sourceOutputDir
  • Dex
    • File outputFolder
  • PackageApplication
    • File resourceFile
    • File dexFile
    • File javaResourceDir
    • File jniDir
    • File outputFile
      • 在Variant对象中修改outputFile属性可以改变最终输出的文件夹.
  • ZipAlign
    • File inputFile
    • File outputFile
      • 在Variant对象中修改outputFile属性可以改变最终输出的文件夹.

每一个任务类型的API由于Gradle的工作方式以及Android插件配置方式而受到限制。首先,Gradle任务只能被配置输入和输出的目录以及一些可能使用到的常量,其次大多数任务的输出都不是固定单一的,一般都混合了sourceSet、Build Type和Product Flavor中的值。这都是为了保证构建文件的简单和可读性,让开发者通过DSL语言去修改构建过程,而不是深入修改任务并改变构建过程。

同时,除了ZipAlign任务,其他类型的任务都需要设置私有数据让它们运行,这就意味着无法手动创建这些类型的新任务。

这些API也有可能被更改,目前大部分API都是围绕着给定任务的输入输出来添加外的处理。

6.4 配置JDK版本(Setting language level)

默认会根据compileSdkVersion来选择JDK版本,可以通过compileOptions设置编译时使用的JDK版本。示例如下:

android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_6
        targetCompatibility JavaVersion.VERSION_1_6
    }
}
作者:xiaohanluo 发表于2017/5/23 16:19:14 原文链接
阅读:181 评论:0 查看评论

View的绘制流程分析之二 -- measure

$
0
0

转载请注明出处:http://blog.csdn.net/crazy1235/article/details/72633385


measure - 测量

确定View的测量宽高

上面说到 performTraversals() 函数的时候,内部调用了 performMeasure()

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

又调用了View中的 measure() 函数!

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

        // 计算key值
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        // 初始化mMeasureCache对象,用来缓存测量结果
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

        // ...

        if (forceLayout || needsLayout) {
            // ...
            // 尝试去查找缓存
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            //没有读取到缓存或者忽略缓存时
            if (cacheIndex < 0 || sIgnoreMeasureCache) { 
                // 测量自己
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else { // 读取缓存
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

           // ...

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        // 缓存
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

来看 onMeasure() 函数

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

setMeasureDimension() 函数倒是很简单!目的就是存储计算出来的测量宽高~

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

现在来主要关注 getDefaultSize()

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

当specMode 是 UNSPECIFIED 的时候,View的宽/高为getDefaultSize()的第一个参数,也就是 getSuggestedMinimumWidth() 或者 getSuggestedMinimumHeight()

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

当View没有背景时,返回mMinWidth,该变量默认值是0,对应android:minWidth这个属性。所以,如果不指定该属性的话,就是0。

如果View有背景,则返回背景的原始宽度。

getSuggestedMinimumHeight()的内部逻辑与getSuggestedMinimumWidth()类似。

所以,当SpecMode是UNSPECIFIED的时候,View的测量宽/高 就是 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 两个方法的返回值!

当SpecMode是AT_MOST 或者 EXACTLY 时,View的测量 宽/高 就是 measureSpec 中的SpecSize ,也就是测量后的大小~


分析到这里,View的measure过程也就分析完了~


那么ViewGroup的measure是什么时候开始的呢???

还得从DecorView来说起!

Activity.attach() 函数中创建了PhoneWindow对象!

public PhoneWindow(Context context, Window preservedWindow) {
        this(context);
        // ...
        if (preservedWindow != null) {
            mDecor = (DecorView) preservedWindow.getDecorView();

            // ... 
    }

在PhoneWindow的构造函数中,创建了DecorView对象!

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks

DecorView继承FrameLayout,所以它是一个ViewGroup!

所以整个window的绘制是从DecorView这个ViewGroup开始的!

mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

这里的mView就是DecorView

其实ViewGroup 是一个抽象类,并没有重写onMeasure()函数!ViewGroup的子类们重写了onMeasure()函数!

既然DecorView继承了FrameLayout,那就拿FrameLayout来分析一下它的 measure过程。


FrameLayout的measure流程

从decorView的measure() 方法体内看出,内部调用了onMeasure()。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 1. 获取子View的个数
        int count = getChildCount();
        // 2. 如果宽/高的SpecMode有一个不是EXACTLY,则为true
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        // 3. 清空集合
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        // 4. 遍历子view
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            // 5. GONE类型的view不测量
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                // 6. 测量子view的宽高
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                // 7. 计算最大宽度
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                // 8. 计算最大高度
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                // 9. 如果measureMatchParentChildren 为true,并且子view设置的宽/高属性是match_parent,就把这个子view添加到集合中
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // 10. 补充计算最大宽度,最大高度
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // 11. 再次比较计算最大宽度,最大高度
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // 12. 如果有背景,则需要再次与背景图的宽高相比较得出最大宽高
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        // 13. 设置当前ViewGroup的测量宽高
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));

        // 14. 遍历需要二次测量的子view
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) { // 子view宽度设置的是MATCH_PARENT
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else { // 子view宽度设置的不是MATCH_PARENT
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) { // 子view高度设置的是MATCH_PARENT
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else { // 子view高度设置不是MATCH_PARENT
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }
                // 15. 测量子view
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

从上面FrameLayout的onMeasure()方法体可以看出来,对子view进行了两次测量,准确的来说不是所有的子view都进行了二次测量~

这是为什么呢?

来往下看~

mMatchParentChildren 是一个集合,是用来存储需要二次测量的子view的!

private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);

那么都有哪些子view需要放到这个集合里面进行二次测量呢?

measureMatchParentChildren == true 的时候!

final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;

那么什么情况下 FrameLayout这个ViewGroup 的宽或者高的SpecMode不为EXACTLY呢?

我在 View的绘制流程分析之一 已经进行了分析!

再来一张图(转载~):

这里写图片描述

抛去UNSPECIFIED 这个mode不管,当前FrameLayout这个ViewGroup的测量模式不为EXACTLY有三种情况!

  1. FrameLayout的父容器的SpecMode为AT_MOST,并且这个FrameLayout的 宽 / 高 属性是match_parent
  2. FrameLayout的 宽 / 高 属性是wrap_content

总而言之,就是FrameLayout这个ViewGroup的宽或高不是一个固定的值,也就是不是EXACTLY模式!

这种情况下,再去判断子view(FrameLayout的子view)的宽高属性是否是match_parent,如果是则把这个子view添加到集合中去!

其实很容易理解!通过第一次遍历所有(不是GONE)的子view之后,就把父布局,也就是这个FrameLayout的宽高给测量出来了。

但是对于那些宽高设置为match_parent,它们的宽高依赖于父容器的大小,所以需要再次遍历它们设置它们的宽高~


measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

先来看看第一次遍历时,对子view的测量过程

直接调用的父类ViewGroup中的方法

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        // 获取布局参数
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        // 计算得出宽度测量规格
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        // 计算得出高度测量规格
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        // 调用子view的measure()函数进行测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

在计算子view的宽度或高度的测量规格时,把父容器的padding值,和子view的margin值 加了上去!

关于getChildMeasureSpec() 这个函数的分析,在上一篇blog已经说过了! getChildMeasureSpec()

那么得到了宽高的测量规格之后,就可以调用measure进行测量了!

child.measure() 调用了View中的measure() 函数,此函数是final类型!不允许子类重写!

其实也很容易理解, 所有的容器测量都要先遍历子view进行测量,然后在确定容器的大小,所以最终实际的任务大多在一个个子view的测量上,容器的大小只需要针对这些子view的测量大小加加减减而已

view的measure过程在本篇blog上面已经进行了分析!本质上还是调用onMeasure() 函数!

如果这个View是一个ViewGroup,则会回调具体的容器类的onMeasure() 函数,如果不是则调用View或者它的子类(如果重写了)的onMeasure() 函数!


此时回过头来再看FrameLayout.onMeasure() 方法体下面这一句

// 13. 设置当前ViewGroup的测量宽高
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));

当对所有的子view测量完毕之后,调用这个函数把计算得来的父容器的测量结果进行保存!

重点在 resolveSizeAndState() 函数!

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) { // 父容器给的尺寸小于测量出来的尺寸,改变result值!
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY: // 精确模式下,结果就是测量的值
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

计算出来size之后,就通过 setMeasuredDimension() 函数保存测量宽高!

这样从ViewGroup到一个个子View的测量,保存每个view/viewGroup的测量宽高!

分析到这里,performTraversals() 函数里面 performMeasure() 也就执行完毕了!

作者:crazy1235 发表于2017/5/23 0:07:15 原文链接
阅读:219 评论:0 查看评论

linux驱动开发之字符设备--内核和用户空间数据的交换(ioctl)

$
0
0

前言

在驱动中,除了需要具备读写能力外,还需要对硬件设备进行控制。ioctl就常用户底层的一些操作。

正文

linux中,建议使用下边的方式,进行进行ioctl命令

设备类型 序列号 方向 数据尺寸
8bit 8bit 2bit 13/14bit

命令码的设备类型为一个 “幻数”,可以是在 0 ~0xff 之间值,内核中的 ioctl-number.txt 给出了一些推荐的和已经被使用的 “幻数”。

命令码列号也是 8 位宽 。

命令码的方向字节也是 2 位宽,该字端表示出具传送的方向,可能的值是 _IO_NONE(无数据传输)、_IOC_READ(读)、_IOC_WRITE(写)、_IOC_READ|_IOC_WRITE(双向)。数据传输的方向是从应用程序的角度来看的

命令码的数据长度字节表示涉及的用户数据的大小,这个成员的宽度依赖于体系结构,通常为 13 或 14 位。

内核中定义了 _IO()、_IOR()、_IOW()、_IOWR()这4个宏来辅助生成命令。

#define _IOC(dir,type,nr,size) \
        (((dir)  << _IOC_DIRSHIFT) | \
         ((type) << _IOC_TYPESHIFT) | \
         ((nr)   << _IOC_NRSHIFT) | \
         ((size) << _IOC_SIZESHIFT))
#define _IO(type,nr)        _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))

以上宏的作用是根据输入的type(设备类型)、nr(序列号)、size(数据长度) 和宏中的位移来生成相应的 命令码。

#define _IOC_SIZE(nr)   \
 ((((((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK) & (_IOC_WRITE|_IOC_READ)) == 0)?    \
                         0: (((nr) >> _IOC_SIZESHIFT) & _IOC_XSIZEMASK))

获得数据域的大小。

应用API

  #include <sys/ioctl.h>
  int ioctl(int d, int request, ...)

参数:fd : 打开的文件描述符
request:命令码
可变参数:可以用来表示,写时用来传入驱动的数据,读时用来接收驱动的数据

内核

    long (*unlocked_ioctl) (struct file *filp , unsigned int, cmd .unsigned long arg);

参数:filp 应用打开的文件在内核的表示
cmd: 对应应用程序传入的命令码
arg : 对于应用程序 传入或者接收的数据

实例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

#include <linux/cdev.h>
#include <linux/fs.h>

#include <linux/uaccess.h>
//幻数
#define IOCTL_TYPE 'b'

#define COUNT   32

struct  ioctl_arg {
    int val;
    char buf[COUNT];
};
//定义的命令码
#define CMDCTL  _IO(IOCTL_TYPE,0)
#define CMDR    _IOR(IOCTL_TYPE,1,struct ioctl_arg)
#define CMDW    _IOW(IOCTL_TYPE,2,struct ioctl_arg)

#define DEVICE_NAME "cdev_demo"

static struct cdev *pdev = NULL;
static int major = 0;
static int minor = 0;
static int count = 2;


#define BUF_SIZE        (1024)
static char kbuf[BUF_SIZE];
static int  buf_count = 0;

static int cdev_demo_open(struct inode * inode, struct file * file)
{
    printk("%s,%d\n",__func__,__LINE__);
    return 0;
}
static int cdev_demo_release(struct inode *inode, struct file * file)
{
    printk("%s,%d\n",__func__,__LINE__);
    return 0;
}
static ssize_t cdev_demo_read(struct file * file, char __user * buffer, size_t size, loff_t * loff)
{
    printk("%s,%d\n",__func__,__LINE__);

    if(0 == buf_count){
        return -EAGAIN;
    }

    if(buf_count < size){
        size = buf_count;
    }

    if(size == copy_to_user(buffer,kbuf,size)){
        return -EAGAIN;
    }

    buf_count  = 0;

    return size;

}

static ssize_t cdev_demo_write(struct file * file, const char __user * buffer, size_t size, loff_t * loff)
{
    printk("%s,%d\n",__func__,__LINE__);
    printk("buffer=%s   size=%d\n",buffer,size);
    if(size >BUF_SIZE){
        return -ENOMEM;
    }
    if(size == copy_from_user(kbuf,buffer,size)){
        return -EAGAIN;
    }

    buf_count = size;
    return size;
}
//ioctl
static long cdev_demo_ioctl (struct file *filep, unsigned int cmd, unsigned long arg)
{
    static struct ioctl_arg buf;
    printk("%s,%d\n",__func__,__LINE__);
    //分辨不同命令码
    switch(cmd){
        case CMDCTL:
                printk("do CMDCTL\n");
                break;
        case CMDR:
            //使用 _IOC_SIZE()获得命令码中的数据长度
            if(sizeof(buf) != _IOC_SIZE(cmd)){
                return -EINVAL;
            }

            if(sizeof(buf) == copy_to_user((struct ioctl_arg*)arg,&buf,sizeof(struct ioctl_arg))){
                return -EAGAIN;
            }
            printk("do CMDR\n");
            break;
        case CMDW:
            if(sizeof(buf)!= _IOC_SIZE(cmd)){
                 return -EINVAL;
            }

            if(sizeof(buf) == copy_from_user(&buf,(struct ioctl_arg*)arg,sizeof(buf))){
                return -EAGAIN;
            }
            printk("do CMDW\n");
            printk("%d,%s \n",buf.val,buf.buf);
            break;

        default:
            break;

    }

    return 0;
}
static struct file_operations fops ={
    .owner   = THIS_MODULE,
    .open    = cdev_demo_open,
    .release = cdev_demo_release,
    .read    = cdev_demo_read,
    .write   = cdev_demo_write,
    .unlocked_ioctl = cdev_demo_ioctl,
};

static int __init cdev_demo_init(void)
{
    dev_t dev;
    int ret;

    printk("%s,%d\n",__func__,__LINE__);

    pdev = cdev_alloc();
    if(NULL == pdev){
        printk("cdev_alloc failed.\n");
        return -ENOMEM;
    }

    cdev_init(pdev,&fops);

    ret = alloc_chrdev_region(&dev,minor,count,DEVICE_NAME);
    if(ret){
        printk("alloc_chrdev_region failed.\n");
        goto ERROR_CDEV;
    }
    major = MAJOR(dev);
    ret = cdev_add(pdev, dev,count);
    if(ret) {
        printk("cdev_add failed.\n");
        goto  ERROR_ADD;
    }

    return 0;
ERROR_ADD:
    unregister_chrdev_region(dev,count);
ERROR_CDEV:
    cdev_del(pdev);
    return ret;
}
static void __exit cdev_demo_exit(void)
{
    printk("%s,%d\n",__func__,__LINE__);
    unregister_chrdev_region(MKDEV(major,minor),count);

    cdev_del(pdev);
    return 0;
}

module_init(cdev_demo_init);
module_exit(cdev_demo_exit);
MODULE_LICENSE("GPL");

APP:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <errno.h>

#include <sys/ioctl.h>


#define IOCTL_TYPE 'b'

#define COUNT   32

struct  ioctl_arg {
    int val;
    char buf[COUNT];
};

#define CMDCTL  _IO(IOCTL_TYPE,0)
#define CMDR    _IOR(IOCTL_TYPE,1,struct ioctl_arg)
#define CMDW    _IOW(IOCTL_TYPE,2,struct ioctl_arg)




#define BUFFER_SIZE  (1024)
int main(int argc,char* argv[])
{
    char *dev = "/dev/cdev_demo";
    int fd = open(dev,O_RDWR);
    if(0 > fd){
        perror("open\n");
        return -1;
    }
    //先构造数据,然后调用ioctl进行写操作
        printf("begin write\n");
        struct ioctl_arg arg = {
            .val = 123,
            .buf = "address",
        };

        if( 0 > ioctl(fd,CMDW,&arg)){
            perror(" write ");
            return -1;
        }

        //调用ioctl进行读取内核数据
        printf("begin read\n");
        struct ioctl_arg args;
        if(0 > ioctl(fd,CMDR,&args)){
            perror("read");
            return -1;
        }
        printf("read args.val = %d args.buf = %s\n",args.val,args.buf);

    close(fd);
    return  0;
}

运行结果:

shell@tiny4412:/mnt # ./test                                                
[  777.455766] cdev_demo_open,39
[  777.457120] do CMDW
[  777.457192] 123,address 
[  777.457238] cdev_demo_ioctl,87
[  777.457334] do CMDR
[  777.457441] cdev_demo_release,44
begin write
begin read
read args.val = 123 args.buf = address

注: [ xx] 表示tiny4412打印的信息,不带的表示app 打印出的信息。

总结

在ioctl中,可以根据命令码对硬件进行功能的操作,即可以进行写操作也可以进行读操作。这样就比read()、write()更加灵活。

作者:u013377887 发表于2017/5/23 20:00:51 原文链接
阅读:15 评论:0 查看评论

kotlin 官方学习教程之包

$
0
0

一个源文件以包声明开始:

package foo.bar

fun baz() {}

class Goo {}

// ...

源文件的所有内容(比如类和函数)都被包声明包括。因此在上面的例子中, bza() 的全名应该是 foo.bar.bza ,Goo 的全名是 foo.bar.Goo。

默认导入

许多包被默认的导入 Kotlin 中:

  • kotlin.*

  • kotlin.annotation.*

  • kotlin.collections.*

  • kotlin.comparisons.* (since 1.1)

  • kotlin.io.*

  • kotlin.ranges.*

  • kotlin.sequences.*

  • kotlin.text.*

其他的包可以根据目标平台来决定是否导入:

  • JVM:

     - java.lang.*
    
     - kotlin.jvm.*
    
  • JS:

     - kotlin.js.*
    

Imports

除了默认导入的包以外, 每个文件都有自己的导入命令。导入语法的声明在 grammar 中描述。

我们可以导入一个单独的名字,例如:

import foo.Bar // Bar 现在可以不用条件就能够使用

或者范围内所有可用的内容(包,类,对象等等):

import foo.* // 'foo' 中的所有都会变成可用的

如果命名有冲突,我们可以使用 as 关键字局部重命名解决冲突

import foo.Bar // Bar 是可用的
import bar.Bar as bBar // bBar 代表 'bar.Bar'

import 关键字不受限于导入类,你也可以用它来导入其它声明:

  • 顶级函数和属性

  • 对象声明中声明的函数和属性

  • 枚举常数

不像 Java,Kotlin 没有 单独的 “import static” 语法,所有这些声明都使用常规 import 关键字导入。

顶级声明的可见性

如果一个顶级声明被标记为 private,那么它在其声明的文件中是私有的(参见 可见度修饰符)。

作者:jim__charles 发表于2017/5/23 13:43:53 原文链接
阅读:34 评论:0 查看评论

十分钟学会kotlin实现Android MVP模式开发

$
0
0

谷歌宣布,将Kotlin语言作为安卓开发的一级编程语言
Google I/O 大会全程视频直播

为什么要学习Kotlin?因为它能使Android的开发更简洁、高效及安全,更因为谷歌的推崇!

不说废话,直入主题。很久之前在看mvp模式的时候,看多很多小例子,这里用kotlin来简单实现一下,完全是入门级的,对于刚刚了解kotlin来开发Android的同学,是个不错的例子。

完整案例和使用Dagger2、Retrofit、RxJava、Kotlin实现MVP的源码欢迎Github查看:
kotlin简单实现MVP之android-mvp源码
Kotlin结合Dagger2等开源项目实现MVP之android-mvp-kotlin源码

案例主要功能是:用户输入用户id、姓名、年龄等信息进行保存,然后通过用户id读取保存的信息。案例很简单,但是涵盖了mvp的各个要素。

MVP: Presenter与Model、View都是通过接口来进行交互的,从而来降低耦合

1.首先,创建一个数据类:
(关于kotlin知识点:1.怎么创建数据类,2.类构造方法)

/**
 * 创建一个数据类
 *
 * Kotlin知识点(data class):http://kotlinlang.org/docs/reference/data-classes.html#data-classes
 *
 * Created by wangdongdong on 17-5-23.
 */
data class User(val userName: String, val age: Int)

2.创建View接口:
(关于kotlin知识点:1.怎么创建一个接口,2.接口方法写法,3.返回值类型,4.方法的参数)

interface IUserView {

    fun getID(): Int
    fun getUsername(): String
    fun getAge(): Int
    fun setUsername(username: String)
    fun setAge(age: Int)
    fun onSaveSuccess()
}

3.创建Model接口

interface IUserModel {
    fun setId(id: Int)
    fun setUsername(username: String)
    fun setAge(age: Int)
    fun save()
    fun load(id: Int): User
}

4.创建Model实现类:
(关于kotlin知识点:1.实现接口的类,2.方法覆写)

class UserModel: IUserModel {

    private var mId: Int = 0
    private var mUsername: String = ""
    private var mAge: Int = 0
    private val mUserArray = SparseArray<User>()

    override fun setId(id: Int) {
        mId = id
    }

    override fun setUsername(username: String) {
        mUsername = username
    }

    override fun setAge(age: Int) {
        mAge = age
    }

    override fun save() {
        mUserArray.append(mId, User(mUsername, mAge))
    }

    override fun load(id: Int): User {
        mId = id
        return mUserArray.get(mId, User("not found", 0))
    }
}

5.创建Presenter接口:

interface IUserPresenter {
    fun saveUser(id: Int, username: String, age: Int)
    fun loadUser(id: Int)
}

6.创建Presenter实现类:

class UserPresenter(val view: IUserView): IUserPresenter {

    private val mUserModel: UserModel = UserModel()

    override fun saveUser(id: Int, username: String, age: Int) {
        Log.d("test_log", "saveUser : $id,$username,$age")
        mUserModel.setId(id)
        mUserModel.setUsername(username)
        mUserModel.setAge(age)
        mUserModel.save()
        view.onSaveSuccess()
    }

    override fun loadUser(id: Int) {
        Log.d("test_log", "loadUser : $id")
        val user = mUserModel.load(id)
        view.setUsername(user.userName)
        view.setAge(user.age)
    }
}

7.创建Activity:

class MainActivity : AppCompatActivity(), IUserView, View.OnClickListener {

    private var mUserPresenter: UserPresenter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mUserPresenter = UserPresenter(this)
        saveButton.setOnClickListener(this)
        loadButton.setOnClickListener(this)
    }

    override fun onClick(v: View) {
        when(v.id) {
            R.id.saveButton -> mUserPresenter?.saveUser(getID(), getUsername(), getAge())
            R.id.loadButton -> mUserPresenter?.loadUser(getID())
        }
    }

    override fun onSaveSuccess() {
        edt_id.setText("")
        edt_username.setText("")
        edt_age.setText("")
        Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show()
    }

    override fun getID(): Int {
        val id = edt_id.text.toString().trim()
        if (id.isNotEmpty())
            return id.toInt()
        else
            return 0
    }

    override fun getUsername(): String = edt_username.text.toString()

    override fun getAge(): Int {
        val age = edt_age.text.toString().trim()
        if (age.isNotEmpty())
            return age.toInt()
        else
            return 0
    }

    override fun setUsername(username: String) {
        Log.d("test_log", "setUsername:$username")
        edt_username.text = toEditable(username)
    }

    override fun setAge(age: Int) {
        Log.d("test_log", "setUsername:$age")
        edt_age.text = toEditable(age.toString())
    }
}

结构很简洁,代码也很简单,相信看完代码后,大家对kotlin的基本使用和mvp模式都能有所了解了!

这里写图片描述

作者:dongdong230 发表于2017/5/23 16:01:49 原文链接
阅读:98 评论:0 查看评论

Android 蓝牙开发(十一)Pan蓝牙共享网络分析

$
0
0

转载请注明出处:http://blog.csdn.net/vnanyesheshou/article/details/71106622

本文已授权微信公众号 fanfan程序媛 独家发布 扫一扫文章底部的二维码或在微信搜索 fanfan程序媛 即可关注

本文主要分析Andorid蓝牙共享网络的使用、连接流程等。
基于Android4.3源码


1 简介

Bluetooth PAN全称:Bluetooth Personal Area Networking,蓝牙个人区域网,是Bluetooth技术的一种重要应用,其核心思想就是用Bluetooth无线技术取代传统的有线电缆,组建个人化信息网络,实现个人范围的资源和信息共享(也就是网络共享)。
主要应用场景:手机与手机、PC与PC、PC与手机之间的网络共享。
三种角色:

NAP(Network Access Point): 如果你的蓝牙设备支持NAP,那么你可以通过共享网络连接来给别的PAN Network内的PC提供上网功能。
GN(Group Ad-hoc Network): 使你可以在小局域网内给其它设备提供数据转发的功能。
PANU(PAN User):与NAP,GN相对的角色,使用NAP,GN提供的功能的设备。

Android上支持作为NAP和PANU角色。


2 NAP

NAP全称NetworkAccessPoint(网络接入点)。网络接入点又称为网络访问点,是带有一个或多个蓝牙射频的装置,作为LAN、GSM等网络和蓝牙网络之间的网桥、代理或路由器的设备。网络接入点为每个相连的蓝牙设备提供了网络服务,如LAN上共享的资源。
要想蓝牙共享网络,首先设备需要连接wifi或者开启流量。然后设置中开启“蓝牙共享网络”。
这里写图片描述
该界面对应着TetherSettings。
路径:packages/apps/Settings/src/com/android/settings/TetherSettings.java
在其onCreate时,会获取BluetoothPan代理对象。

BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter != null) {
    //获取pan代理对象
    adapter.getProfileProxy(activity.getApplicationContext(), 
            mProfileServiceListener,
            BluetoothProfile.PAN);
}

getProfileProxy是异步的,获取成功、失败会回调mProfileServiceListener。代码如下:

private BluetoothProfile.ServiceListener mProfileServiceListener =
    new BluetoothProfile.ServiceListener() {
    public void onServiceConnected(int profile, BluetoothProfile proxy) {
        //获取成功
        mBluetoothPan.set((BluetoothPan) proxy);
    }
    public void onServiceDisconnected(int profile) {
        //获取失败
        mBluetoothPan.set(null);
    }
};

打开蓝牙共享会调用到startTethering()。如果蓝牙状态为关闭,则先开启蓝牙。保证蓝牙为开启状态,然后打开蓝牙共享。

BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
//判断蓝牙状态
if (adapter.getState() == BluetoothAdapter.STATE_OFF) {
    mBluetoothEnableForTether = true;
    adapter.enable(); //打开蓝牙
    mBluetoothTether.setSummary(R.string.bluetooth_turning_on);
    mBluetoothTether.setEnabled(false);
} else {
    BluetoothPan bluetoothPan = mBluetoothPan.get();
    //打开蓝牙共享
    if (bluetoothPan != null) bluetoothPan.setBluetoothTethering(true);
    mBluetoothTether.setSummary(R.string.bluetooth_tethering_available_subtext);
}

bluetoothPan.setBluetoothTethering(true)跳到Bluetooth应用中,
代码路径:packages/apps/Bluetooth/src/com/android/bluetooth/pan/PanService.java
先调用到内部类BluetoothPanBinder的setBluetoothTethering方法。

public void setBluetoothTethering(boolean value) {
    PanService service = getService();
    if (service == null) return;
    service.setBluetoothTethering(value);
}

该方法中很明显是去调用PanService的setBluetoothTethering方法。

void setBluetoothTethering(boolean value) {
    enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
    if(mTetherOn != value) {
        mTetherOn = value;  //保存状态
        //在更改状态时,删除任何现有的panu、pan-nap连接
        List<BluetoothDevice> DevList = getConnectedDevices();
        for(BluetoothDevice dev : DevList)
            disconnect(dev);
    }
}

该函数主要是将状态值保存喜爱,然后将之前的连接都断开。
打开网络共享后就可以进行连接互联网共享了,当A设备打开网络共享后,点击互联网访问,连接B设备一直失败,这样是错误的,应该B设备主动连接该设备,而不是该设备连接其他设备。

当别的设备与NAP设备(该设备)连接成功,NAP会回调com_android_bluetooth_pan.cpp中的connection_state_callback(),然后回调PanService中的onConnectStateChanged(),然后跳到handlePanDeviceStateChange中,在该函数中由于远端设备为PANU,本地设备为NAP,则会调用enableTethering,用来配置ip地址等相关信息(具体还不是很清楚)。handlePanDeviceStateChange会向为发送广播,携带蓝牙共享的相关状态。


3 PANU

PANU Personal Area Networking user,个人区域网用户。
要想连接别的设备的蓝牙共享网络,首先需要配对,配对成功后,在已配对界面点击“互联网访问”来连接远端设备,
这里写图片描述
该操作经过一系列调用,跳到PanProfile中的connect方法中。具体如何跳到PanProfile中的connect方法,可以参考如下文章。
http://blog.csdn.net/vnanyesheshou/article/details/71106622
http://blog.csdn.net/vnanyesheshou/article/details/71811288
PanProfile中的connect会跳到Bluetooth应用中的PanService中。
在PanService的connect函数中判断与该设备的连接状态是否断开,没有断开则返回false。断开着向handler发送消息,返回true。handler中处理该消息如下:

BluetoothDevice device = (BluetoothDevice) msg.obj;
//进行pan连接
if (!connectPanNative(Utils.getByteAddress(device),
        BluetoothPan.LOCAL_PANU_ROLE, BluetoothPan.REMOTE_NAP_ROLE)) {
    //连接失败,发送状态
    handlePanDeviceStateChange(device, null, BluetoothProfile.STATE_CONNECTING,
            BluetoothPan.LOCAL_PANU_ROLE, BluetoothPan.REMOTE_NAP_ROLE);
    handlePanDeviceStateChange(device, null,
            BluetoothProfile.STATE_DISCONNECTED, BluetoothPan.LOCAL_PANU_ROLE,
            BluetoothPan.REMOTE_NAP_ROLE);
    break;
}

调用connectPanNative进行pan的连接,返回false则表示失败,向外发送广播(CONNECTING、DISCONNECTED);返回true则表示该操作成功,等待连接状态回调。
connectPanNative函数中的参数,看出本地设备作为PANU角色,远端作为NAP角色,所以上面作为NAP时主动连接其他设备失败。
connectPanNative为native方法,其会跳到com_android_bluetooth_pan,然后向hardware层调用。

连接状态回调与上相同。都会回调到handlePanDeviceStateChange中。

//网络相关,暂不清楚是怎么回事。
LinkProperties lp = new LinkProperties();
lp.setInterfaceName(iface);             mTetherAc.sendMessage(NetworkStateTracker.EVENT_NETWORK_CONNECTED, lp);

然后向外发送连接状态的广播。该广播只能判断pan协议的连接状态,并不能代表是否能正常共享网络。因为NAP端设备可能没有连接网络,或者分配ip地址等没有成功。

public boolean isPanConnected(Context context){
    ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo btInfo = cm.getNetworkInfo(ConnectivityManager.TYPE_BLUETOOTH);
    if(btInfo!=null){
        return btInfo.isConnected();
    }
    return false;
}

上述代码可以判断与NAP设备的网络是否连接,但并不能保证可以上网,连接成功后,NAP设备不管可不可以上网,上述代码都返回true。暂时还没有找到其他方法。

欢迎扫一扫关注我的微信公众号,定期推送优质技术文章:

这里写图片描述

作者:VNanyesheshou 发表于2017/5/23 20:21:57 原文链接
阅读:9 评论:0 查看评论

Android开发中SQLite Expert的操作技巧

$
0
0

最近学习SQLite Expert,了解过后,才觉得和MySql同工异曲,深度学习android开发,就必须了解Sqlite Expert,才能更上一层楼。

在之前的一篇文章中我已经粗浅的介绍了SQLite的基本使用方法,这一骗我重点介绍一下SQlite的事务操作,适配器操作,分页操作等技巧.

当然,先简单的温习一下Sqlite基本方法,http://blog.csdn.net/google_huchun/article/details/71105034

学习SQLite ,就必须学习语法技巧,对query要有很深的理解,才能进步。

    SQLiteDatabase db = SQLiteDb.getWritableDatabase();
                /**
                 * query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)
                 * String table 表示查询的表名
                 * String[] columns 表示查询表中的字段名称,null 查询所有
                 * String selection 表示查询条件 where子句
                 * String[] selectionArgs 表示查询条件占位符的取值
                 * String groupBy 表示分组条件 group by 子句
                 * String having 表示率选条件 having 子句
                 * String orderBy 表示排序条件 order by 子句
                 */
                Cursor cursor=db.query(Constant.TABLE_NAME, null, Constant._ID+">?", new String[]{"10"}, null, null, Constant._ID+" desc");
                List<Person> list = DbManager.cursorToList(cursor);
                for (Person p : list){
                    Log.i("tag", p.toString());
                }
                db.close();

这是query使用说明。

增删改查是SQLite的基本使用方法,认识后,再来看SQLite事务操作,用代码介绍;

SQLiteDatabase db = mHelper.getWritableDatabase();
    // 开启事务
    db.beginTransaction();
    for (int i=1;i<=100;i++){
        String sql = "replace into "+ Constant.TABLE_NAME+ " values("+i+", 'hucc"+i+"', 1822220)";
        db.execSQL(sql);
    }
    // 设置事务标志成功,当结束事务时就会提交事务
    db.setTransactionSuccessful();
    // 结束事务
    db.endTransaction();
    db.close();

解释一下,这里使用的replace 代替了 insert 方法,因为使用insert容易引起
Android:android.database.sqlite.SQLiteConstraintException:UNIQUE constraint failed。

SQlite使用的适配器,是SimpleCursorAdapter和CursorAdapter,而Cursor游标就相当于Java中的HashMap一样,使用起来很方便。

SimpleCursorAdapter:

  /**
     * 将数据源数据加载到适配器中
     * SimpleCursorAdapter(Context context, int list_item, Cursor c, String[] from, int[] to, int flags)
     *  Context context 上下文对象
     *  int list_item 表示适配器控件中每项item 的布局id
     *  Cursor c 表示Cursor 数据源
     *  String[] from 表示cursor 中数据表字段的数组
     *  int[] to 表示展示字段对应值的控件资源id
     *  int flags 设置适配器的标记
     */
 SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.list_item, cursor, new String[]{Constant._ID, Constant.NAME, Constant.PHONE}, new int[]{R.id.tv_id, R.id.tv_name,R.id.tv_phone}                                 SimpleCursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);

CursorAdapter:

 /**
      *  以内部类的定义形式适配器
 */
  /**
     *  表示创建适配器控件中每个item对应的view对象
     * @param context 上下文
     * @param cursor  数据源cursor对象
     * @param parent  当前item的父布局
     * @return  每项item的view对象
     */
    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        View view = LayoutInflater.from(context).inflate(R.layout.list_item,null);
        return view;
    }

/**
     *  通过newview()方法确定了每个item展示的view对象,在bindview()对布局中的控件进行填充
     * @param view 由newView()返回的每项view对象
     * @param context  上下文
     * @param cursor  数据源cursor对象
     */
    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        TextView tv_id = (TextView) view.findViewById(R.id.tv_id);
        TextView tv_name = (TextView) view.findViewById(R.id.tv_name);
        TextView tv_phone = (TextView) view.findViewById(R.id.tv_phone);

        tv_id.setText(cursor.getInt(cursor.getColumnIndex(Constant._ID))+"");
        tv_name.setText(cursor.getString(cursor.getColumnIndex(Constant.NAME)));
        tv_phone.setText(cursor.getInt(cursor.getColumnIndex(Constant.PHONE))+"");
    }

常用的数据适配器就是以上两种,这是我根据自己的程序张贴出该模块。

当然了,也可以使用BaseAdapter操作,下来我会介绍这个适配器。

说起数据,我们使用数据就是要处理数据,根据数据才能持续开发,数据的分页操作是SQLite的重点。这里我就用ListView+BaseAdapter来介绍。

数据的分页需要自定义几类参数,

private int totalNum; //表示加载当前控件加载数据总条目
private int pageSize = 20; // 表示当前数据条目
private int pageNum; // 表示总页码
private int currentPage = 1; // 当前页码
private boolean isDivPage;
private List<Person> totalList;

在onCreate()初始化数据操作,

// 获取数据表数据总数目
    totalNum = DbManager.getDataCount(db,Constant.TABLE_NAME);
    // 根据总条目与每页展示数据条目,获得总页数
    pageNum = (int) Math.ceil(totalNum / pageSize);
    if (currentPage == 1){
         totalList = DbManager.getListByCurrentPage(db,Constant.TABLE_NAME,currentPage,pageSize);
    }

 final MyBaseAdapter mAdapter = new MyBaseAdapter(this, totalList);
    mListView.setAdapter(mAdapter);

    mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            if (isDivPage && AbsListView.OnScrollListener.SCROLL_STATE_IDLE == scrollState){
                if (currentPage<pageNum){
                    currentPage++;
                    // 根据最新页码加载获取集合存储到数据源中
                    totalList.addAll(DbManager.getListByCurrentPage(db,Constant.TABLE_NAME, currentPage,pageSize));
                    mAdapter.notifyDataSetChanged();
                }
            }
        }
        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
            isDivPage = ((firstVisibleItem + visibleItemCount)==totalItemCount);

        }
    });

这是我事先操作了一个数据表”info.db”,添加在assets中,这样就需要打开该数据表,

private SQLiteDatabase openDatabase(Context context) {
    File file = new File(DATABASE_PATH);
    //查看数据库文件是否存在
    if (file.exists()){
        Log.i("test", "存在数据库");
        // 存在则直接返回打开的数据库
        return SQLiteDatabase.openOrCreateDatabase(file, null);
    }else{
        //不存在先创建文件夹
        File path = new File(DATABASE_PATH);
        Log.i("test", "pathStr="+path);
        if (path.mkdir()){
            Log.i("test", "创建成功");
        }else{
            Log.i("test", "创建失败");
        }
        try {
            //得到资源
            AssetManager am = context.getAssets();
            //得到数据库的输入流
            // InputStream is = am.open(Constant.DATABASE_NAME);
            // InputStream is = am.open("info.db");
            InputStream is = am.open(String.valueOf(R.raw.info));
            Log.i("test", is+"");
            //用输出流写到SDcard上面
            FileOutputStream fos = new FileOutputStream(file);
            Log.i("test", "fos="+fos);
            Log.i("test", "file="+file);
            //创建byte数组  用于1KB写一次
            byte[] buffer = new byte[1024];
            int count = 0;
            while ((count = is.read(buffer))>0){
                Log.i("test", "得到");
                fos.write(buffer, 0, count);
            }
            fos.flush();
            fos.close();
            is.close();
        }catch (IOException e){
            e.printStackTrace();
            return null;
        }
    }
    return openDatabase(context);
}

就是代码,就是如何打开sqlite的步骤。数据的分页操作就需要根据数据表名称获取数据总条目:

  /*
  *  id 数据库对象
 *   tableName 数据表名称
 *   数据总条目
 */
public static int getDataCount(SQLiteDatabase db, String tableName){
    int count = 0;
    if (db != null){
        Cursor cursor = db.rawQuery("select * from "+tableName, null);
        count = cursor.getCount(); // 获取cursor中的数据总数
    }
    return count;

另外需要根据当前页码查询获取该页码的聚合数据.

 /*
* @param db  数据库对象
 * @param tableName  数据表名称
 * @param currentPage  当前页码
 * @return  当前页对应的集合
 *
 *  select * from person ?,?  如何根据当前页码获取该页数据
 */
public static List<Person> getListByCurrentPage(SQLiteDatabase db, String tableName, int currentPage, int pageSize){
    int index = (currentPage-1)*pageSize; // 获取当前页码第一条数据的下标
    Cursor cursor = null;
    if (db != null){
        String sql = "select * from "+tableName+" limit ?,?";
        cursor = db.rawQuery(sql, new String[]{index+"", pageSize+""});
    }
    return cursorToList(cursor);   //将查询的cursor对象转换成list集合
}

需要更多的知识可以关注本人的代码库:

https://git.oschina.net/huchun/sqliteexpert

这些就是数据库的高级技巧,当然了还有更多的使用技巧没有介绍出,本人能力有限嘛,还在继续学习中,如有好什么疑问,我们再继续交流,探讨技术,一起提高!

作者:Google_huchun 发表于2017/5/23 22:49:03 原文链接
阅读:655 评论:0 查看评论

一周入门Kotlin(上)

$
0
0

Kotlin是最近IO大会推荐的一级语言,也是我们学习的主流方向,其语法实际上不难,融合了很多优秀语言的特性,如面向过程C语言思想。OC语言的代理和类扩展思想。lambda表达式的精简等等。接下来你会想如何学习这个东西又不浪费时间呢。本教程以java语言的思维进行转换学习,一周带你入门Kotlin。

搭建环境

这里我使用的是mac环境,但是我想应该没多大问题,首先该语言是android studio3.0全面支持的,其下载地址是Android Studio预览版。如果在2.3的工具中使用如下教程

Kotlin的技术地址做了大量的介绍,你可以在官网查看技术的实现.

  1. 创建新的应用成功
  2. 找到android studio 的action工具
    这里写图片描述
  3. 将java工程自动转换成kotlin工程
    这里写图片描述
  4. 转换后的结果如下
    这里写图片描述

类的使用

声明一个简单类

1.创建kotlin文件,如果创建 你会发现该文件的后缀是kt
这里写图片描述
2.现在我们创建一个Person类,代码如下

class Person {

    //1.声明了2个变量 他们的类型分别是String和Kootlin中的Int
    //2.在Kotlin中,基本类型已经不能使用 只能使用kotlin支持的Int Double Long 等
    //3.var和val都代表声明一个对象 val声明的对象是无法重新赋值
    //4.声明的属性必须默认先赋一个默认的值
    //5.内部自动为属性创建setter+getter方法
    val name: String =""
    var age: Int =0

}

创建一个子类

现在,我们接着创建一个学生类 该类是Person的子类。这里有一点要主要的,默认如果一个类被别的类继承,那么他必须添加open关键字

open class Person {

    //声明了2个变量 他们的类型分别是String和Kootlin中的Int
    var name: String =""
    var age: Int =0

}

Student类的创建如下,如果你创建了Person类,系统会为你默认创建一个空参构造器:

// 这里可以看出Student类继承Person并继承Person的无参构造器
class Student : Person() {

    // 默认每个学生都有3个课程
    var courses :Int =3
    // 默认每个学生身高1.70米
    var height: Double =1.70

}

构造器创建

在Kotlin中,构造器分为2种,一种是主构造器,一种是次构造器。主构造器是在类创建的时候顺便指定的,比如我们想在Student类创建的时候指定名称和课程数,那么类的创建如下:

//1.在构造器创建的参数也会让该类内部自动为属性创建setter+getter方法
//2.因为name在父类已经声明过了 这里不需要var关键字
class Student(name: String ,var courses :Int =3) : Person() {

    // 默认每个学生身高1.70米
    var height: Double =1.70

}

那么问题来了,如果你想在一个类中创建多个次构造器,可以这么写:

class Student(name: String ,var courses :Int =3) : Person() {

    // 默认每个学生身高1.70米
    var height: Double =1.70

    //1.这里的构造必须使用constructor关键字 必须重写主构造器
    //2.主构造器我们不传递courses就使用默认的3
    //3.次构造器默认不创建setter+getter方法,也不能有val/var关键字,它不会在该类中创建属性
    //4.如果该构造器不用赋值 你完全可以去掉方法体的{ }
    constructor(nameValue:String,ageValue:String):this(nameValue)

}

方法的创建

学生应该有学习行为,这里没有方法的说法 我们称为函数。所以我们可以为学生创建一系列的方法:

class Student(name: String, var courses: Int = 3) : Person() {

    var height: Double = 1.70

    //1.fun代表创建一个study函数 传入课程名courseName 返回成绩 类型为Int
    //2.每个语句都没有;作为结尾 这是他的特点
    fun study(courseName: String): Int {
        if (courseName.equals("语文")){
            return 99
        }
        return 80
    }

    //如果一个方法返回一个数据并只用了一行代码 那么可使用如下写法
    fun printf():String = "$name 的身高是 $height"

    //如果一个方法不返回数据 可以用Unit代替 你可以认为是void 当然:Unit也可以不写
    fun play():Unit{
        Log.i("IT520","$name 正在玩!")
    }

}

setter/getter方法

默认情况下属性会自动生成setter/getter方法 这类似于OC的做法。但是我们也可以重写这两个方法,只需要在属性后面声明即可,而field指向的就是该属性,因为kotlin不运行直接方法该对象。

class Student(name: String, var courses: Int = 3) : Person() {

    var height: Double = 1.70
        get() = field
        set(value) {
            field =value +0.01
        }
}

类静态变量

一般在java语言中如果使用静态变量,必须使用static关键字,如果想初始化静态代码块,可以使用static{ }.kotlin提供了类型的功能,比如我们想在Student中创建3个课程的常量,可以这么写:

class Student(name: String, var courses: Int = 3) : Person() {
    //该模块下的代码在大部分其他类中都可以调用
    companion object{
        public val MATH : Int =1
        public val CHINESE : Int =2
        public val ENGLISH : Int =3
    }
}

对象的使用

对象的创建

还记得上面我们创建的构造器吗?

class Student(name: String, var courses: Int = 3) : Person() {
    ...
}

创建的代码可以如下,var代表声明一个变量:

//1.创建对象没有了new关键字
//2.构造器第二个参数就算不传递 也可以使用默认的
//3.每句代码都没有;
var s1=Student("lean")
var s2=Student("zhangsan",5)
//4.如果一个对象不想马上创建 可以引向null 但是声明必须加? 表示该对象可以为null
var s3: Student? = null
//5.如下添加了?表示 如果s3对象为null 则不调用
s3?.printf()

调用setter/getter方法

安卓大部分setter/getter方法都可以

class Student(name: String, var courses: Int = 3) : Person() {

    var height: Double = 1.70
        get() = field
        set(value) {
            field =value +0.01
    }

}

//调用内部的setter
s2.height=1.75
//调用getter
val height: Double=s2.height
//打印某个变量
Log.i("IT520","$height")

扩展类的功能

有时候 你使用的第三方库 或者官方的代码用的不爽 如果是java语言的话你可以使用包装类来添加功能,但是kotlin还提供了类似OC的一个功能如下:

operator fun ViewGroup.get(index: Int): View = getChildAt(index)

对,operator操作符就是扩展方法的操作符,该句代码放在某个文件中即可。这里我们扩展的ViewGroup类,为他添加一个get方法。

Kotlin Android Extensions

Android Extensions是有Kotlin官方提供的一个可以帮助我们在代码区获取控件并处理的工具,其网址:Kotlin Android Extensions

  1. 在build.gradle 文件的最上面添加:

        apply plugin: 'kotlin-android-extensions'
  2. 在代码类中添加

    import kotlinx.android.synthetic.main.布局名.*
  3. 如果在布局中有个控件

    <TextView
         android:id="@+id/hello"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:text="Hello World, MyActivity" />
  4. 在onCreate()方法中,可以这样调用:

    hello.setText("Hi!")
作者:qq285016127 发表于2017/5/24 0:36:57 原文链接
阅读:155 评论:0 查看评论

iOS 本地轻量级数据存储之NSUserDefaults

$
0
0

NSUserDefaults简介

在Android和ios都提供了本地轻量级数据存储,底层实现都是基于key-value的方式。Android里面的叫SharedPreferences,ios的叫NSUserDefaults,使用上也是非常的简单。我们先看一看NSUserDefaults的API。

#import <Foundation/NSObject.h>

@class NSArray<ObjectType>, NSData, NSDictionary<KeyValue, ObjectValue>, NSMutableDictionary, NSString, NSURL;

NSUserDefaults 是一个单例,所以就不存在全局问题,需要用到的时候直接取就可以,随时存取,十分方便。
NSUserDefaults支持的数据类型有:NSNumber(Integer、Float、Double),NSString,NSDate,NSArray,NSDictionary,BOOL。

NSUserDefaults实例

下面我们以一个第一次登录记住密码的功能来讲讲NSUserDefaults。比如说我们要存储
用户名(NSString):userName
密码(NSNumber):userPassword
用户信息(NSDictionary):userInfo

那么我们可以定义如下:

    NSString *userName=@"xzh";
    NSNumber *userPassword=@123456;
    NSDictionary *userInfo=@{
                             @"age":@29,
                             @"sex":@"male"
                             };
    BOOL isOn=YES;

接着我们使用NSUserDefaults开始对资料进行存储。

 [[NSUserDefaults standardUserDefaults] setObject:userName forKey:@"name"];
    [[NSUserDefaults standardUserDefaults] setInteger:[userPassword integerValue] forKey:@"password"];
    [[NSUserDefaults standardUserDefaults] setObject:userInfo forKey:@"info"];
    [[NSUserDefaults standardUserDefaults] setBool:isOn forKey:@"isOn"];

 //调用synchronize存储
    [[NSUserDefaults standardUserDefaults] synchronize];

取数据的方法跟存是一样的,取出数据。

NSString *userName=[[NSUserDefaults standardUserDefaults] objectForKey:@"name"];
    NSInteger userPassword=[[NSUserDefaults standardUserDefaults] integerForKey:@"password"];
    NSDictionary *userInfo=[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"info"];
    BOOL isOn=[[NSUserDefaults standardUserDefaults] boolForKey:@"isOn"];

一些其他的方法,如根据键移除某一数据等。

//移除某一键值对
    [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"name"];
//或者设置为空
    [[NSUserDefaults standardUserDefaults] setObject:nil 
//获取所有的数据
    NSDictionary *allDic=[[NSDictionary alloc]initWithDictionary: [[NSUserDefaults standardUserDefaults]dictionaryRepresentation]];

NSUserDefaults只能存储一些简单的数据类型,如果要存取一些复杂类型的数据(例如实体类),就需要对数据进行归档后转为NSData后存取。

//存一个类 ClassA
ClassA *bc = [[ClassA alloc] init];     
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];     
NSData *saveObject = [NSKeyedArchiver archivedDataWithRootObject:bc];     
[ud setObject:saveObject forKey:@"ClassA"];     
//下面是取出
NSData *getObject = [ud objectForKey:@"myBusinessCard"];    
ClassA *bcNew = [[ClassA alloc] init];   
bcNew = [NSKeyedUnarchiver unarchiveObjectWithData:getObject] ;
作者:xiangzhihong8 发表于2017/5/24 8:33:46 原文链接
阅读:176 评论:0 查看评论

Android应用优化之代码检测优化

$
0
0

前言

最近换了新的公司,面对新的代码大家都有不同的熟悉过程和方法。在我的角度来说,利用代码检测工具,可以更直接地去熟悉代码逻辑和业务逻辑,表现得自己去代码质量很有追求,最重要当然是在公司的任务管理工时上面显得自己积极向上啦。不过在修改代码之前,你要根据项目的分工、明确在公司的定位,不然会造成一些不愉快的事情,但是总的来说我们还是对代码质量有追求的!

我们首先要知道Android Studio安装插件步骤。

这里写图片描述

Android Lint

Lint是Android Studio提供的一个代码检测工具,开发者通过它不用写测试代码,就可以发现和纠正问题,优化代码结构。

Lint 的使用路径:
工具栏(或右击package)-> Analyze -> Inspect Code

待分析完毕后,我们可以在Inspection栏目中看到检查的结果

这里写图片描述
Lint被检测到的问题,都有描述基本信息并指明相应的严重性级别,我们可以根据问题的严重程度进行处理优化。Android Lint内置了很多lint规则,共可以分为以下几类:

  • correctness 正确性
  • Security 安全性
  • Performance 性能
  • Usability 可用性
  • Accessibility 可访问性
  • Internationalization 国际化

自定义Android Lint的检查提示

在xml编写布局的时候,例如TextView,我们常常会直接将text字符串值直接写在xml上面,还是在textSize属性上写上sp值,然而,IDE会有相关的提示:

这里写图片描述

看到这个提示,大家觉得级别过低不会太多理会,其实这并不是一个好的编码习惯,所以我们可以通过更改对应的severity等级来更改提示的等级,如: 默认hardcode的severity等级为warning,我们修改hardcode的severity等级为error,那么在存在硬编码时候将会以error等级提醒我们:

这里写图片描述

修改完成后,我们可以看到text提示使用红色错误的波浪线标记了,如下图,但是我们这个修改是提示,我们看上端文件并没有标错,所以是不会影响程序运行的。

这里写图片描述

Lint还有很多自定义的设置,大家有兴趣可以看一下这篇文章自定义 Lint 规则如何帮助开发者,然后去尝试一下。

另外由于lint的规则过多,我们没有必要每一个都要知道,我们需要在检测分析后,能区分出这个是属于什么类型的错误和严重程度,根据给出的错误提示,或直接使用搜索引擎/神器 stackoverflow进行对应的错误搜索并解决。

FindBugs

FindBugs使用静态分析方法为出现bug模式检查Java字节码。FindBugs基本上只需要一个程序来做分析的字节码,所以这是非常容易使用。它能检测到常见的错误,如NullPointException,多线程同步问题等。

FindBugs 的使用路径:
工具栏(或右击package)-> FindBugs -> Analyze对应目标

这里写图片描述

FindBugs支持对包级别、项目级别、模块级别、单个文件级别、自定义范围的Bug分析。

代码缺陷分类:

这里写图片描述

  • Bad practice:不好的做法;
  • Malicious code vulnerbility:恶意的代码漏洞;
  • Correctness:正确性问题;
  • Performance:潜在的性能问题;
  • Security:安全性问题;
  • Dodgy code:糟糕的代码;
  • Experimental:实验;
  • Multithreaded correctness:多线程的正确性多线程编程时,可能导致错误的代码;
  • Internationalization:国际化问题;

FindBugs检测之后,当我们选中的错误,在右方有十分详细的描述,我们可以根据给出的错误类对应的行数、方法、错误基础描述、优先级的信息进行优化解决。

这里写图片描述

当然大家对FindBugs的错误描述有兴趣,可以浏览一下FindBugs Bug Descriptions,而解决方案我们利用神器 stackoverflow

这里写图片描述

Checkstyle

Checkstyle是一个开发工具用来帮助程序员编写符合代码规范的Java代码。这个工具能够帮助你在项目中定义和维持一个非常精确和灵活的代码规范形式。

相信大家做过几个不同的项目后,会发现不同的开发者有着不同的代码风格习惯,有的是教科书式规范,有的是像行外人般的随心所欲。所以好的代码风格对开发是事半功倍的。站在一个管理者的角度,Checkstyle对整一个开发的水平管理很有帮助,曾听说某项目管理在code review时发现代码不符合规范直接辞退开发。站在一个开发者的角度,一个规范的代码风格,看着代码就感觉舒畅,再听着小歌,写代码简直就能飞起来。现实一点就是,人家看你的代码风格就知道你是个大神。在最近的阿里出品的Java开发手册,得到了业内很好的响应。良好的代码风格让我们的水平不断向BAT靠近。下图值得你拥有。

这里写图片描述

Checkstyle检测范围:

  • javadoc注释
  • 命名规范
  • 标题
  • 导入包规范
  • 体积大小
  • 空格
  • 修饰符
  • 代码块
  • 编码问题
  • 类设计问题
  • 重复、度量以及一些杂项

Checkstyle使用步骤

1.安装CheckStyle-IDE插件

2.添加检查规则文件

这里写图片描述

这里写图片描述

3.CheckStyle-IDE 插件使用

这里写图片描述

同样我们可以对项目级别、模块级别、单个文件级别进行检测。

4.CheckStyle扫描结果

这里写图片描述

5.根据Checkstyle扫描结果对应修改
重复3.~5.部,至修改完成。

Checkstyle定制

我觉得定制属于自己公司的checkstyle检测规则是十分重要的。我们可以看一下Google checkstyle 检查规则

Checkstyle检查规则是基于XML配置文件的,主要通过XML文件中的module 、property、message标签节点对检查规则进行配置。在XML文件中,被指定的module,都将被对应规则检查。

1.module节点
module 主要是指检查项,如MethodName (检查方法命名)

module中有两个比较重要的节点,它们分别是Checker(checkStyle配置文件的根节点,必须存在)、TreeWalker(树遍历器),TreeWalker会自动去检查指定范围内的每一个java源文件,TreeWalker内部会定义很多module。

module的根节点是Checker,一定要有;

2.property节点
对应module 检查项中具体检查属性,如果使用默认值,property节点可以省略;

3.message节点
checkStyle检查出来,是否打印出message消息,message节点可以省略.

4.Checkstyle-IDE进行扫描后,让Checkstyle不对部分代码进行规则检查.
在定制好的checkStyle.xml文件中,添加一个名为SuppressionFilter的moudle,在过滤规则文件suppressions.xml中添加相应的过滤规则。

Checkstyle有很多玩法,有兴趣的同学可以看一下Checkstyle官网资料Github CheckStyle源码总而言之,Checkstyle助我们技(丧)术(心)升(病)华(狂)!

PMD

PMD(注意搜索的关键字用QAPlug - PMD)是一个很有用的工具,它跟Findbugs类似,但是它不同与FindBugs检测字节码,它是直接检测源代码。PMD也是使用静态分析方式来发现错误。因此根据它们的检测方式不同,我们可以将PMD和FindBugs结合一起使用。

PMD同样有好多rule,且适用多种语言。如Android目前有三个规则:

Android (java)

  • CallSuperFirst: Super should be called at the start of the method(检查在Activity或Service里的子类里,是否在错误位轩调用父类onCreate等应该放在方法前的方法。)
  • CallSuperLast: Super should be called at the end of the method(检查一些应该在方法结束时才调用父类实现的情况。)
  • DoNotHardCodeSDCard: Use Environment.getExternalStorageDirectory() instead of “/sdcard”(你应该用Environment.getExternalStorageDirectory()而不是硬编码去取得扩展存储目录。)

PMD使用步骤

1.在build.gradle里加入plugin

apply plugin: 'pmd'

2.定义任务,ruleSets是需要检查的一些规则,有兴趣的同学可以看看官方定义的rule,按需来添加。

task pmd(type: Pmd) {
    source fileTree('src')

     ruleSets = [
            'java-android'
            ···
    ]
    reports {
        xml.enabled = false
        html.enabled = true
        xml {
            destination "$reportsDir/pmd/pmd.xml"  //这里是报告产生的路径
        }
        html {
            destination "$reportsDir/pmd/pmd.html"  //这里是报告产生的路径
        }
    }
}

3.Gradle运行自定义任务,在对用的路径下找到检测结果。

总结

其实工具和方法很多,而且很多工具和方法有重复功能,大家都可以根据需要修改和调整。另外同学觉得本篇并没有针对常用的问题,进行分析和贴出解决方案。我个人建议大家可以在stackoverflow尽情翻阅,看看歪果仁是怎么去交流和解决问题的。

由于本人刚入职不久的小开发,通篇是站在一个开发员工角度来描述,使用的是插件的形式去描述使用,如果对一个公司的代码质量管理,可以利用Gradle配置,结合SonarQube等工具进行管理和code review。

个人习惯是,我首先用Lint应用于一些基础检测,再用FindBugs检测一些潜在的Bug,PMD较少用。然而Checkstyle是需要一直使用,对代码风格的规范。

我们一定要清楚我们的目的,制定规范和使用规范的是为了让团队能一起愉快的写代码,让其他同事能简单地对项目进行维护。

作者:qq_435559203 发表于2017/5/24 14:58:09 原文链接
阅读:18 评论:0 查看评论

Jenkins持续化构建Android项目(一)-安装配置Jenkins(by 星空武哥)

$
0
0

转载请标注原创文章地址:http://blog.csdn.net/lsyz0021/article/details/72681857

1、安装配置Jenkins

2、构建Android项目生成apk

3、Jenkins上传apk到fir

4、Jenkins上传apk到蒲公英

5、设置Jenkins邮件通知

前言

前两天公司构建Android项目的Jenkins出了点问题,于是就登陆到Jenkins看了看,通过查看日志,很快就定位到了问题。解决完问题后,感觉Jenkins持续化自动构建Android项目不错,其实之前早就想了解一下Jenkins构建Android项目了,正好凑着这个机会研究了一下,搞明白之后,于是乎在CSDN专门开设了一个“Jenkins自动化构建Android项目”的专栏来发表这几篇文章。

我的两个专栏地址:

Jenkins自动化构建Android项目:http://blog.csdn.net/column/details/15653.html

Ubuntu下搭载Android开发环境:http://blog.csdn.net/column/details/15672.html

1、下载安装Jenkins

在看下面的文章时,默认你已经搭建好Android开发环境了,并且已经配置好环境变量了(jdk、sdk、gradle环境变量都要配置,并且已经配置好了tomcat)

下面的命令测试通过就说明环境变量配置好了,分别用下面的命令测试

java -version
gradle -version
android

配置完上面环境变量后,去官网下载Jenkins:https://jenkins.io/download/ ,这里有两种安装方式:一个是安装包,一种是war包,war包是放在tomcat的webapps目录下即可,运行tomcat后他会自动解压,建议这种是用这种方式。本教程也是使用的war的形式。

2、配置Jenkins

下载完成后,将war包放到tomcat的”webapps”目录下,然后运行tomcat,待tomcat启动后在”webapps”目录下就可以看到”jenkins”目录,然后再浏览器内输入:http://localhost:8080/jenkins/ 登陆Jenkins。

首次登陆后需要重置初始密码,找到个人文件目录,Jenkins自动生成的初始密码

只有在登陆首次登陆时需要输入这个默认的密码

下一步是让你选择要安装的插件,这里我们建议选择”建议安装的插件”,因为这里面几乎包含我所需要的插件了

必须要有下面的这些插件(如果你选择“Install suggested plugin”,默认就包含了)

SSH Credentials Plugin
Gradle plugin

下一步就是设置登陆密码了,设置完Jenkins就算安装完成了
打开浏览器输入:http://localhost:8080/jenkins/
然后输入登陆密码就可以进入到Jenkins了

拿出微信 扫码关注下面的微信订阅号,及时获取更多推送文章

作者:lsyz0021 发表于2017/5/24 17:24:26 原文链接
阅读:115 评论:0 查看评论

Kotlin语法学习-变量定义、函数扩展、Parcelable序列化、编写工具类、Activity跳转

$
0
0

今年 Google I/O 2017 开发者大会中,Google 宣布正式把 Kotlin 纳入 Android 程序的官方一级开发语言(First-class language),作为Android开发者,当然要逐步熟悉这门语言,第一步就要从语法开始学习。

在这之前,我们需要了解怎么使用Kotlin编写一个Android应用。对于Android Studio 3.0版本,我们在创建工程的时候直接勾选 Include Kotlin support 选项就可以了;对于3.0以前的版本,我们需要安装Kotlin插件,同时还要手动配置gradle,方法如下

  • 在app的gradle下加入如下代码
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
  • 在project的gradle下加入如下代码
ext.kotlin_version = '1.1.2-3'

classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

Kotlin定义变量

  1. kotlin 里的变量定义有两种,val 和 var,其中 val 等同 Java 中 final 修饰的变量(只读),一般是常量,var一般是变量。

  2. kotlin 的变量定义支持赋值时类型推断,且所有变量默认被修饰为「不可为 null」,必须显式在类型后添加 ? 修饰符才可赋值为 null。

  3. 我们写代码时要尽量习惯性地将变量设计为不可为空,这样在后面对该变量的运算中会减少很多问题。

Kotlin函数扩展

具体的语法是fun + 类型.函数(参数)

    fun Context.toast(message: String, length: Int = Toast.LENGTH_SHORT) {
        Toast.makeText(this, message, length).show()
    }

Kotlin Parcelable序列化

package com.john.kotlinstudy

import android.os.Parcel
import android.os.Parcelable

/**
 * Java Bean 数据实体类
 * Created by john on 17-5-24.
 */

data class UserBean(var name: String, var id: String) : Parcelable {

    constructor(source: Parcel) : this(source.readString(), source.readString())

    override fun describeContents(): Int {
        return 0
    }

    override fun writeToParcel(dest: Parcel, flags: Int) {
        dest.writeString(this.name)
        dest.writeString(this.id)
    }

    companion object {

        @JvmField val CREATOR: Parcelable.Creator<UserBean> = object : Parcelable.Creator<UserBean> {
            override fun createFromParcel(source: Parcel): UserBean {
                return UserBean(source)
            }

            override fun newArray(size: Int): Array<UserBean?> {
                return arrayOfNulls(size)
            }
        }
    }
}

companion关键字解读

  1. 不像 Java 或者 C#,在 Kotlin 中,Class 没有静态方法,在大多数情况下,推荐用 package-level 的函数来代替静态方法。

  2. 如果你需要写一个不需要实例化 Class 就能访问 Class 内部的函数(例如一个工厂函数),你可以把它声明成 Class 内的一个实名 Object。

  3. 另外,如果你在 Class 内声明了一个 companion object,在该对象内的所有成员都将相当于使用了 Java/C# 语法中的 static 修饰符,在外部只能通过类名来对这些属性或者函数进行访问。

@JvmField 注解作用

  1. 指示Kotlin编译器不为此属性生成getter / setter,并将其作为一个字段暴露出来。

  2. 如果您需要在Java中公开Kotlin属性作为字段,则需要使用@JvmField注释对其进行注释,该字段将具有与底层属性相同的可见性。

Kotlin 编写工具类

在Java中,我们会将一些常用的功能封装成一个个工具类,工具类其实就是对于String,Collection,IO 等常用类的功能的扩展。我们写的工具类方法和变量都会写成静态的。因为,这些方法我们只是想调用一下,不需要牵扯工具类中的任何属性和变量,所以就没有必要实例化了(new),既然不需要实例化了,那么就用静态就行了。

package com.john.kotlinstudy

import android.content.Context
import android.widget.Toast

/**
 * Toast工具类
 * Created by john on 17-5-24.
 */
object ToastUtils {

    fun toast(context: Context, message: String) {
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
    }
}

Kotlin Activity 跳转

我们在MainActivity设置点击事件,跳转到另一个Activity,同时传递数据过去

package com.john.kotlinstudy

import android.content.Context
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        test_tv.text = "hello kotlin"
        test_tv.setOnClickListener {
            ToastUtils.toast(this, "hello kotlin")
            val user = UserBean("zhang", "001")
            user.id = "100"
            SecondActivity.navigateTo(this, user)
        }
    }

    fun Context.toast(message: String, length: Int = Toast.LENGTH_SHORT) {
        Toast.makeText(this, message, length).show()
    }
}

然后新建一个SecondActivity,提供一个静态方法,用于Activity的跳转。想必大家都知道这样做的好处,就是让调用者不必看源码就知道需要什么参数。如果你按照java写,就会发现没有static这个关键字!不要慌,这里可以使用伴生对象来实现,伴生对象是伴随这个类声明周期的对象。

package com.john.kotlinstudy

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_second.*

/**
 * 跳转Activity测试类
 * Created by john on 17-5-24.
 */
class SecondActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        val user = intent.getParcelableExtra<UserBean>(EXTRA_KEY_USER)
        user_name_tv.text = user.name
        ToastUtils.toast(this, user.id)
    }

    //创建一个伴生对象
    companion object {
        //extra的key
        val EXTRA_KEY_USER = "extra.user"

        fun navigateTo(context: Context, user: UserBean) {
            val intent = Intent(context, SecondActivity::class.java)
            intent.putExtra(EXTRA_KEY_USER, user)
            context.startActivity(intent)
        }
    }
}

小结

以上只是简单的介绍了kotlin一些语法特性,算是入门,对这门新语言消除些许陌生恐惧,其实kotlin有很多新特性,这还需要我们在开发中慢慢消化理解。

作者:johnWcheung 发表于2017/5/24 17:28:48 原文链接
阅读:629 评论:0 查看评论

Jenkins持续化构建Android项目(二)-构建Android项目生成apk(by 星空武哥)

$
0
0

转载文章请标注原创文章地址:http://blog.csdn.net/lsyz0021/article/details/72681988

1、安装配置Jenkins

2、构建Android项目生成apk

3、Jenkins上传apk到fir

4、Jenkins上传apk到蒲公英

5、设置Jenkins邮件通知

Jenkins安装完我们就可以新建项目了
首先开始新建一个项目,选择类型为“构建一个自由风格的软件项目”

新建完成,就是配置项目

源码管理
我们要从git仓库拉去代码,需要添加代码路径
如果使用SSH,不使用账号和密码,那么就要先配置SSH,
可以参考这篇文章配置SSH :Windows 7下Git SSH 创建Keyhttp://blog.csdn.net/lsyz0021/article/details/52064829

构建触发
我们选择Poll SCM模式,每十分钟拉取一次代码,如果代码发生了改变,就自动构建

构建步骤选择下面的“Invoke Gradle script”

配置“Invoke Gradle script”

Task里面填写gradle命令,显示clean,再执行打包命令,
然后在设置项目路径,安装图配置即可。

配置完后,点击保存,就可以构建了

查看构建日志
到页面最底部,可以看到构建成功了

在用户的目录就可以看到成的apk了,下面我们就将这个apk上传到fir

拿出微信 扫码关注下面的微信订阅号,及时获取更多推送文章

作者:lsyz0021 发表于2017/5/24 17:34:40 原文链接
阅读:139 评论:0 查看评论

Jenkins持续化构建Android项目(三)-Jenkins上传apk到fir(by 星空武哥)

$
0
0

转载文章请标注原创文章地址:http://blog.csdn.net/lsyz0021/article/details/72683171

1、安装配置Jenkins

2、构建Android项目生成apk

3、Jenkins上传apk到fir

4、Jenkins上传apk到蒲公英

5、设置Jenkins邮件通知

方法一

如果还没有fir账号,请先注册一个fir账号:https://account.fir.im/users/sign_up,注册完后生成需要的API token

下载Jenkins需要的fir上传插件:http://7xju1s.com1.z0.glb.clouddn.com/fir-plugin-1.9.5.hpi

然后启动Jenkins安装下载的插件:找到“系统管理”——>“管理插件”

进入“管理插件”后,点击“高级”——>“上传插件”

安装完成后,在刚才新建项目中找到“配置”选项,然后“构建”选项中增加“Upload to fir.im”

填入刚才复制的API token和apk路径绝对路径(这里的路径也可以不用填写,他会自动搜索的)

配置完后保存,手动构建一次

进入fir账户,可以看到上传成功了

方法二

如果你用的是linux系统,我们还以使用另一种方式上传,并且更简单。上一步用的是 “Upload to fir.im” 插件,我们也可不使用这插件,而是用“Execute shell”命令。

详细命令

find app/build/outputs/apk -name "*.apk" -exec fir publish -T APItoken {} \;

拿出微信 扫码关注下面的微信订阅号,及时获取更多推送文章

作者:lsyz0021 发表于2017/5/24 17:47:03 原文链接
阅读:138 评论:0 查看评论

Jenkins持续化构建Android项目(四)-上传apk到蒲公英(by 星空武哥)

$
0
0

转载文章请标注原创地址:http://blog.csdn.net/lsyz0021/article/details/72683242

1、安装配置Jenkins

2、构建Android项目生成apk

3、Jenkins上传apk到fir

4、Jenkins上传apk到蒲公英

5、设置Jenkins邮件通知

1、注册账号,获取取API Key和User Key

如果没有蒲公英的账号,需要先注册一个账号:https://www.pgyer.com/user/register

然后在“账户设置”中找到“API信息”,获取API Key和User Key

2、安装curl

我们用到了curl上传,所以需要先安装curl
下载地址:https://curl.haxx.se/download.html#Win64

安装完成后需要配置curl环境变量,然后在cmd中输入

curl --version

下面说明安装成功了

3、配置上传命令

在“构建”的“增加构建步骤”中添加“Execute shell”,然后配置下面的上传命令
(注意:这里用到了curl命令,所以要先配置下载并且配置curl的环境变量:)

curl -F "file=@C:\app-release.apk" -F "uKey=b604" -F "_api_key=5e68fe3a" https://www.pgyer.com/apiv1/app/upload

配置完手动构建一次即可开始上传

拿出微信 扫码关注下面的微信订阅号,及时获取更多推送文章

作者:lsyz0021 发表于2017/5/24 17:52:53 原文链接
阅读:145 评论:0 查看评论

Jenkins持续化构建Android项目(五)-设置Jenkins邮件通知(by 星空武哥)

$
0
0

转载请标注原创文章地址:http://blog.csdn.net/lsyz0021/article/details/72683275

1、安装配置Jenkins

2、构建Android项目生成apk

3、Jenkins上传apk到fir

4、Jenkins上传apk到蒲公英

5、设置Jenkins邮件通知

Jenkins邮件通知一般有两种:一种是自带的,一种是插件。由于第一种方式的只能构建失败时候才发生邮件,所有使用较少,因此我们直接使用第二种。

如果想使用邮箱发送功能,必须先设置邮箱的SMTP服务功能,以下是网易163邮箱(不同邮箱设置可能有所不同)的设置完的界面

邮箱设置完后,就要设置Jenkins了,首先进入Jenkins“系统设置”界面

找到“Extended E-mail Notification”,这是就是配置邮箱的地方,先配置SMTP服务器,再点击“高级”按钮设置邮箱的账号和密码。

填入该邮箱的账号和密码,“Default Recipients”是默认接收邮件的邮箱地址

点击“Default Triggers”按钮,设置什么情况下发送邮件

这是我选择的一种情况

Jenkins的“系统设置”算是设置完成了,然后点击“保存”,进入到项目的设置

进入项目设置

在项目配置的“构建后操作”选项卡中,我们新增“Editable Email Notification”(E-mail Notification是系统默认的邮件通知一般不用)

“project recipient list”:设置项目收件人名单邮件列表
“Content Type”:设置内容类型
“Default Subject”:默认主题
“Default Content”:默认内容

设置完成后,手动构建一下,构建完成应该就可以收到发送的邮件了

下面是我的Jenkins“系统设置”的全部设置

拿出微信 扫码关注下面的微信订阅号,及时获取更多推送文章

作者:lsyz0021 发表于2017/5/24 17:56:01 原文链接
阅读:146 评论:0 查看评论

iOS开发学习专题-基础知识(五) NSDate时间 NSUserDefaults本地存储 NSNotification系统通知的详细使用方式

$
0
0

本文主要讲解的是 NSDate时间NSUserDefaults本地存储NSNotification系统通知的详细使用方式,也是NS系列基础知识的最后一篇文章

文章是博主原创,转载请标明出处http://blog.csdn.net/werctzzz/article/details/72677981

NSUserDefaults是iOS系统提供的一个单例类(iOS提供了若干个单例类),通过类方法standardUserDefaults可以获取NSUserDefaults单例。
NSUserDefaults适合存储轻量级的本地数据。存储的数据在关闭程序之后,再次运行的时候依然存在,它的数据存储在/Library/Prefereces沙盒路径下.

// NSUserDefaults支持数据格式:
//    NSNumber
//    NSString
//    NSArray
//    NSDictionary
//    BOOL
//    NSDate
// NSUserDefaults提供的快捷对象存储方式:
//    setBool:  forKey:
//    setFloat: forKey:
//    setInteger:   forKey:
//    setDouble:    forKey:
//    setURL:   forKey:
    
    // 1.初始化:
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    
    // 2.NSUserDefaults操作,和字典一样通过key值来存入和读取数据
    // 2.1 创建存入的数据
    [defaults setInteger:23 forKey:@"myInteger"];
    // 2.2 数据存入磁盘
    [defaults synchronize];
    // ⭐️方法synchronise是为了强制存储,其实并非必要,因为这个方法会在系统中默认调用,但是你确认需要马上就存储,这样做是可行的。
    
    // 2.3 通过key值读取数据
    NSLog(@"%ld",[defaults integerForKey:@"myInteger"]);
    
    // 3.因为NSUserDefaults所存储的对象和值是不可变的(只能覆盖)
    // 3.1 所以在存可变的类型时候我们需要这样
    NSMutableArray *mutableArray1 = [NSMutableArray arrayWithObjects:@"123",@"234", nil];
    NSArray * array = [NSArray arrayWithArray:mutableArray1];
    NSUserDefaults *user = [NSUserDefaults standardUserDefaults];
    [user setObject:array forKey:@"Array"];
    // 3.2 在读取的时候我们也是需要这样来操作
    NSMutableArray *mutableArray2 = [NSMutableArray arrayWithArray:[user objectForKey:@"记住存放的一定是不可变的"]];
    
    // 4.我们可以试试存储图片(上期讲过的NSData)
    UIImage *image1 =[UIImage imageNamed:@"aaimg"];
    NSData *imageData1 = UIImageJPEGRepresentation(image1, 100);//把image归档为NSData
    [user setObject:imageData1 forKey:@"image"];
    // 读取
    NSData *imageData2 = [user dataForKey:@"image"];
    UIImage *image2 = [UIImage imageWithData:imageData2];

引用“iOS 提供了一种 "同步的" 消息通知机制,观察者只要向消息中心注册, 即可接受其他对象发送来的消息,消息发送者和消息接受者两者可以互相一无所知,完全解耦。这种消息通知机制可以应用于任意时间和任何对象,观察者可以有多个,所以消息具有广播的性质,只是需要注意的是,观察者向消息中心注册以后,在不需要接受消息时需要向消息中心注销,这种消息广播机制是典型的“Observer”模式。这个要求其实也很容易实现. 每个运行中的application都有一个NSNotificationCenter的成员变量,它的功能就类似公共栏. 对象注册关注某个确定的notification(如果有人捡到一只小狗,就去告诉我). 我们把这些注册对象叫做 observer. 其它的一些对象会给center发送notifications(我捡到了一只小狗). center将该notifications转发给所有注册对该notification感兴趣的对象. 我们把这些发送notification的对象叫做 poster。消息机制常常用于在向服务器端请求数据或者提交数据的场景,在和服务器端成功交互后,需要处理服务器端返回的数据,或发送响应消息等,就需要用到消息机制。”

- (void)NSNotificationUseFunction{
    // 通知模式NSNotification(也称消息中心)
    
    // 1.注册接收的通知者
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test:) name:@"testNotification" object:nil];
    // 1.1发送通知
    [self sendUsername];
    
    // 系统通知
    // 2.APP切换到后台
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(enterBack:) name:UIApplicationDidEnterBackgroundNotification object:nil];
    
    // 3.APP切换回前台
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(enterFace:) name:UIApplicationWillEnterForegroundNotification  object:nil];
}

// 1.1通知发送者
- (void)sendUsername{
    // 创建要发送的数据
    NSDictionary * data = [NSDictionary dictionaryWithObject:@"我是名字" forKey:@"username"];
    // 向中心发送数据
    [[NSNotificationCenter defaultCenter]postNotificationName:@"testNotification" object:nil userInfo:data];
}
// 1.2接收通知的内容
- (void)test:(NSNotification*)testNotifi{
    NSDictionary * data = [testNotifi userInfo];
    NSString * userName = [data objectForKey:@"username"];
    NSLog(@"username:%@",userName);
}
// 2.App切换到后台
- (void)enterBack:(NSNotification*)enterBackNotifi{
    NSLog(@"进入到后台");
}
// 3.App切换到前台
- (void)enterFace:(NSNotification*)enterFaceNotifi{
    NSLog(@"进入到前台");
}

NSDate 是苹果提供的一个类,在iOS开发中用于处理时间和日期

// 获取当前日期
    // 1.初始化
    NSDate *date = [NSDate date];
    NSLog(@"当前时间 date = %@",date);
    // 2.改变时间
    // 获取从某个日期开始往前或者往后多久的日期,此处60代表60秒,如果需要获取之前的,将60改为-60即可
    date = [[NSDate alloc] initWithTimeInterval:60 sinceDate:[NSDate date]];
    NSLog(@"当前时间 往后60s的时间date = %@",date);// 可以和上面的打印时间做比较
    
    // 3.当地时间的获取,
    // 应为iOS默认是格林尼治时间,所以需要用NSTimeZone矫正时间时区,NSTimeZone会获取本机定位的区域用来获取时区
    NSTimeZone *zone = [NSTimeZone systemTimeZone];
    NSInteger interval = [zone secondsFromGMTForDate: date];
    NSDate *localDate = [date  dateByAddingTimeInterval: interval];
    NSLog(@"当前地区时间 localDate = %@",localDate);
    
    // 4.时间的格式化
    NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
    [formatter setDateFormat:@"yyyy"];
    NSInteger currentYear=[[formatter stringFromDate:date] integerValue];
    [formatter setDateFormat:@"MM"];
    NSInteger currentMonth=[[formatter stringFromDate:date]integerValue];
    [formatter setDateFormat:@"dd"];
    NSInteger currentDay=[[formatter stringFromDate:date] integerValue];
    NSLog(@"当前时间 = %@ ,年 = %ld ,月=%ld, 日=%ld",date,currentYear,currentMonth,currentDay);
    
    // 5.NSDate与NSString
    NSDateFormatter *dateFormatter =[[NSDateFormatter alloc] init];
    // 5.1设置日期格式
    [dateFormatter setDateFormat:@"年月日 YYYY/mm/dd 时间 hh:mm:ss"];
    NSString *dateString = [dateFormatter stringFromDate:date];
    NSLog(@"dateString = %@",dateString);
    // 5.2设置日期格式
    [dateFormatter setDateFormat:@"YYYY-MM-dd"];
    NSString *year = [dateFormatter stringFromDate:date];
    NSLog(@"年月日 year = %@",year);
    // 5.3设置时间格式
    [dateFormatter setDateFormat:@"hh:mm:ss"];
    NSString *time = [dateFormatter stringFromDate:date];
    NSLog(@"时间 time = %@",time);
    // 可以看看打印结果是什么~
    
    // 6.日期的比较NSDate的比较
    // 当前时间
    NSDate *currentDate = [NSDate date];
    // 比当前时间晚一个小时的时间
    NSDate *laterDate = [[NSDate alloc] initWithTimeInterval:60*60 sinceDate:[NSDate date]];
    // 比当前时间早一个小时的时间
    NSDate *earlierDate = [[NSDate alloc] initWithTimeInterval:-60*60 sinceDate:[NSDate date]];
    // 比较哪个时间迟
    if ([currentDate laterDate:laterDate]) {
        NSLog(@"current-%@比later-%@晚",currentDate,laterDate);
    }
    // 比较哪个时间早
    if ([currentDate earlierDate:earlierDate]) {
        NSLog(@"current-%@ 比 earlier-%@ 早",currentDate,earlierDate);
    }
    if ([currentDate compare:earlierDate]==NSOrderedDescending) {
        NSLog(@"current 晚");
    }
    if ([currentDate compare:currentDate]==NSOrderedSame) {
        NSLog(@"时间相等");
    }
    if ([currentDate compare:laterDate]==NSOrderedAscending) {
        NSLog(@"current 早");
    }
总算在百忙之中把NS常用类的基础知识整理完了,如果有什么写不对,写不全的地方,欢迎大家来留言反馈。作为知识共享,首先要自己能明白,自己能有自己的想法,然后再给大家展现自己的想法,希望能抛砖引玉~之后会开始整理UI部分以及各种小知识点~敬请期待

作者:werctzzz 发表于2017/5/24 18:33:09 原文链接
阅读:315 评论:0 查看评论

IOS端K线系列之K线-边框绘制、滑动选择

$
0
0

k线系列目录

查看目录请点击这儿


在分时线写完以后,我们开始接着学习如何写K线。其实k线并没有想象的那么复杂,还是像前几篇文章提供的思路一样,第一步、第二步、第三步…….把一个复杂的问题简单化,才是我们最需要做的事情。

首先看一下最终要完成的效果图:

K线

不管是现货还是股票类的k线,都是一样的。因为k线本质上是用来表示某个商品价格变动的情况(如果不了解k线基础知识,点击这儿)。上图的k线是由一根根蜡烛组成,分为主图、副图、主图指标、副图指标四部分,其中主图中还包含日期部分。

滑动的选择

Tip:如果读这一小节的内容感觉到云里雾里时,千万不要着急,其实完全可以略过这小节内容,跟着文章的思路往下走,等做完这部分内容时,可以再回顾一下。

在绘制之前,我们来讨论一个重要的问题,也是这篇文章说的一个重点,就是关于滑动的选择。

经过使用Reveal对市面上多个app的查看,以及自己在开发中踩了好多坑,在这里提供两种方式:

  1. 单个View
  2. 单个View + ScrollView

第一种指的是在主副图View上添加滑动手势,然后根据坐标产生的位移来实时刷新主副图View上的蜡烛。

第二种指的是在主副图View上方盖一个ScrollView,然后用户滑动ScrollView,根据ScrollView产生的偏移量来实时刷新主副图View上的蜡烛。

当选用第一种方式时,因为是添加滑动手势来获取的偏移量,所以这个偏移量不是非常线性,给用户的感觉是滑动起来不顺畅。解决办法是获取偏移量时,需要多次调试,每次获取的偏移量需要判断范围以及增加合适的倍数,尽量能保证View获得的偏移量线性。但使用这种方式的好处是不增加其他控件,在视图层次上很清晰。

当选用第二种方式时,用户能感知到的滑动体验很好,会感觉非常流畅。但有一个缺点不容忽视,那就是ScrollView的ContentSize是随着加载的蜡烛数量的增加而变大的,因为只有ContentSize和蜡烛数量相对应时,才可以滑动到最左或最右。所以,当一次性加载的蜡烛数量过高,会导致一个巨大的ScrollView存在。

边框的绘制

当明确了我们要达到的效果后,我们也可以仿照效果图把k线分为4部分:主图指标、主图、副图指标、副图。这里默认k线4部分是在同一个View上,并且是在这个view上面添加滑动手势。

绘制线段的方法在画分时线的文章中就已经讲过,这里不再重复。直接上代码:

//设置主图、主图指标、副图、副图指标rect
    _mainIndexRect = CGRectMake(0, 0, CGRectGetWidth(self.frame), mainIndexH);
    _mainRect = CGRectMake(0, mainIndexH, CGRectGetWidth(self.frame), (CGRectGetHeight(self.frame) - (mainIndexH + accessoryIndexH + dateH)) * mainFrameScale);
    _accessoryIndexRect = CGRectMake(0, mainIndexH + CGRectGetHeight(_mainRect)+dateH, CGRectGetWidth(self.frame), accessoryIndexH);
    _accessoryRect = CGRectMake(0, mainIndexH + CGRectGetHeight(_mainRect)+dateH+accessoryIndexH, CGRectGetWidth(self.frame), (CGRectGetHeight(self.frame) - (mainIndexH + accessoryIndexH + dateH)) * (1-mainFrameScale));


    CAShapeLayer *borderLayer = [CAShapeLayer layer];
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.bounds];

    [path moveToPoint:CGPointMake(0, mainIndexH)];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), mainIndexH)];

    [path moveToPoint:CGPointMake(0, CGRectGetMaxY(_mainRect))];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetMaxY(_mainRect))];

    [path moveToPoint:CGPointMake(0, CGRectGetMinY(_accessoryIndexRect))];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetMinY(_accessoryIndexRect))];

    [path moveToPoint:CGPointMake(0, CGRectGetMinY(_accessoryRect))];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetMinY(_accessoryRect))];

    float mainUnitH = CGRectGetHeight(_mainRect) / 4.f;
    float mainUnitW = CGRectGetWidth(_mainRect) / 4.f;

    for (int idx = 1; idx <= 3; idx++)
    {
        //画3条横线
        [path moveToPoint:CGPointMake(0, mainIndexH + mainUnitH * idx)];
        [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), mainIndexH + mainUnitH * idx)];

        //画3条竖线
        [path moveToPoint:CGPointMake(idx * mainUnitW, mainIndexH)];
        [path addLineToPoint:CGPointMake(idx * mainUnitW, CGRectGetMaxY(_mainRect))];

        //画3条竖线
        [path moveToPoint:CGPointMake(idx * mainUnitW, CGRectGetMinY(_accessoryRect))];
        [path addLineToPoint:CGPointMake(idx * mainUnitW, CGRectGetMaxY(_accessoryRect))];
    }

    float accessoryUnitH = CGRectGetHeight(_accessoryRect) / 2.f;
    [path moveToPoint:CGPointMake(0, CGRectGetMaxY(_accessoryRect) - accessoryUnitH)];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetMaxY(_accessoryRect) - accessoryUnitH)];

    borderLayer.path = path.CGPath;
    borderLayer.lineWidth = 0.5f;
    borderLayer.strokeColor = [UIColor blackColor].CGColor;
    borderLayer.fillColor = [UIColor clearColor].CGColor;

    [self.layer addSublayer:borderLayer];

代码执行效果如下:

k线边框

需要源码的话,点这里

作者:yunkai666 发表于2017/5/24 18:36:43 原文链接
阅读:230 评论:0 查看评论

Flutter进阶—实现动画效果(五)

$
0
0

在本篇文章开始前,我们先来回顾一下之前我们都做了哪些事情。在第一篇文章中,我们在动画值更改时调用double lerpDouble(num a, num b, double t)重新绘制条形。在第二篇文章中,我们首先用Tween类帮助我们管理动画值,并重新绘制条形,然后把绘制条形动画相关的类提取到bar.dart文件。在第三篇文章中,我们首先在Bar类中增加颜色的字段,再新建color_palette.dart文件,用于获取颜色值,同时用工厂构造函数Bar.empty和Bar.random分别创建空白Bar实例和随机Bar实例。在第四篇文章中,我们新增了BarChart类,用于创建指定数量的Bar实例列表,并将绘制条形的代码更改为绘制条形图。

接下来,我们为Bar类增加x坐标和宽度属性,然后我们使BarChart支持具有不同列数的图表。我们的新图表将适用于数据集,其中bar i代表某些系列中的第i个值,如产品发布后第i天的销售额。这样的图表涉及0..n个条形,但一个图表的条形数量n可能不同于下一个图表。

比如有两个图表,分别有5个和7个条形。5个条形的表格可以按照之前的方法进行动画化。bars的索引5和6在另一个动画终点没有对方,但是现在我们可以自由地给每个条形自己的位置和宽度,我们可以引入两个不可见的条形来扮演这个角色。视觉上效果是随着动画的进行,bars的索引5和6成长为最终的外观。如果是相反方向的动画,则bars的索引5和6将会减弱或淡入隐形。

复合值之间的线性插值(lerp)通过相应的组件相关联,如果某个组件在一个终点丢失,则在其位置使用一个不可见组件。通常有几种方法来选择不可见的组件,假设我们的产品经理决定使用零宽度、零高度的条形,其x坐标和颜色从其可见对象继承,我们将为Bar添加一个方法来创建给定实例的collapsed版本。

import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';
import 'dart:ui' show lerpDouble;
import 'dart:math';
import 'color_palette.dart';

class BarChart {
  final List<Bar> bars;

  BarChart(this.bars);

  factory BarChart.empty(Size size) {
    return new BarChart(<Bar>[]);
  }

  factory BarChart.random(Size size, Random random) {
    const barWidthFraction = 0.75;
    const minBarDistance = 20.0;
    // floor():返回不大于此的最大整数
    final barCount = random.nextInt((size.width/minBarDistance).floor()) + 1;
    final barDistance = size.width / (1+barCount);
    final barWidth = barDistance * barWidthFraction;
    final startX = barDistance - barWidth/2;
    final color = ColorPalette.primary.random(random);
    final bars = new List.generate(
      barCount,
      (i)=> new Bar(
        startX + i * barDistance,
        barWidth,
        random.nextDouble() * size.height,
        color,
      ),
    );
    return new BarChart(bars);
  }

  static BarChart lerp(BarChart begin, BarChart end, double t) {
    // max:返回两个数字中较大的一个
    final barCount = max(begin.bars.length, end.bars.length);
    final bars = new List.generate(
      barCount,
      (i) => Bar.lerp(
        // ??:如果为空时取的默认值
        begin._barOrNull(i) ?? end.bars[i].collapsed,
        end._barOrNull(i) ?? begin.bars[i].collapsed,
        t,
      )
    );
    return new BarChart(bars);
  }

  Bar _barOrNull(int index) => (index<bars.length ? bars[index] : null);
}

class BarChartTween extends Tween<BarChart> {
  BarChartTween(BarChart begin, BarChart end) : super(begin: begin, end: end);

  @override
  BarChart lerp(double t) => BarChart.lerp(begin, end, t);
}

class Bar {
  Bar(this.x, this.width, this.height, this.color);
  final double x;
  final double width;
  final double height;
  final Color color;

  Bar get collapsed => new Bar(x, 0.0, 0.0, color);

  static Bar lerp(Bar begin, Bar end, double t) {
    return new Bar(
        lerpDouble(begin.x, end.x, t),
        lerpDouble(begin.width, end.width, t),
        lerpDouble(begin.height, end.height, t),
        Color.lerp(begin.color, end.color, t)
    );
  }
}

class BarTween extends Tween<Bar> {
  BarTween(Bar begin, Bar end) : super(begin: begin, end: end);

  @override
  Bar lerp(double t) => Bar.lerp(begin, end, t);
}

class BarChartPainter extends CustomPainter {
  BarChartPainter(Animation<BarChart> animation)
      : animation = animation,
        super(repaint: animation);

  final Animation<BarChart> animation;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = new Paint()..style = PaintingStyle.fill;
    final chart = animation.value;
    for(final bar in chart.bars) {
      paint.color = bar.color;
      canvas.drawRect(
        new Rect.fromLTWH(
          bar.x,
          size.height - bar.height,
          bar.width,
          bar.height
        ),
        paint
      );
    }
  }

  @override
  bool shouldRepaint(BarChartPainter old) => false;
}

将上述代码整合到我们的应用程序中,包括为此新设置重新定义BarChart.random和BarChart.empty。现在可以合理地使用空白图表来包含空图表零条形,而随机的条形图可以包含所有相同随机颜色的随机数量的条形,并且每个具有随机选择的高度。但是由于位置和宽度现在是Bar定义的一部分,我们需要BarChart.random来指定这些属性。为BarChart.random提供图表Size参数是合理的,可以缓解BarChartPainter.paint的大部分计算。

最后我们需要更新main.dart文件,让我们的应用程序可以重新显示。

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  static const size = const Size(200.0, 100.0);
  // ...
  @override
  void initState() {
    // ...
    tween = new BarChartTween(
      new BarChart.empty(size),
      new BarChart.random(size, random));
    animation.forward();
  }
  // ...
  void changeData() {
    setState(() {
      tween = new BarChartTween(
        tween.evaluate(animation),
        new  BarChart.random(size, random),
      );
      animation.forward(from: 0.0);
    });
  }
  // ...
  }
}

这里写图片描述

未完待续~~~

作者:hekaiyou 发表于2017/5/24 19:19:36 原文链接
阅读:216 评论:0 查看评论

Android测试-- Uiautomatorviewer

$
0
0

uiautomatorviewer同样位于sdk/tools/bin下,
用一下你就可以捕获当前设备当前画面截图
然后还有节点和坐标~聪明的你操作操作坐标就行了
这里写图片描述

作者:u013867301 发表于2017/5/24 23:02:30 原文链接
阅读:149 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>