Android自定義Gradle插件的詳細(xì)過程
一、Gradle
我們知道在我們現(xiàn)在使用Android Stduio開發(fā)Android項(xiàng)目的時(shí)候,Android Studio是基于Gradle來幫助我們構(gòu)建,管理項(xiàng)目的。
Gradle:Gradle是一個(gè)項(xiàng)目構(gòu)建工具,用來幫助我們管理項(xiàng)目的依賴、打包、發(fā)布、部署等工作。
Gradle是通過如build.gradle這種gradle腳本來進(jìn)行項(xiàng)目構(gòu)建的,所以我們對項(xiàng)目的構(gòu)建配置都是可以寫在gradle構(gòu)建腳本中。
gradle構(gòu)建腳本使用的是Groovy語言,Groovy語言也是一種jvm語言,最終也是編譯成class文件然后在jvm上執(zhí)行,所以所有的Java語言的特性Groovy都支持,我們可以完全混寫Java和Groovy;
Gradle插件:
Gradle是一個(gè)構(gòu)建項(xiàng)目的工具,但是Gradle本身只提供了一個(gè)核心框架,各種構(gòu)建功能,比如編譯、依賴、打包等都是通過插件來完成的;
比如我們使用構(gòu)建Android項(xiàng)目,就需要可以構(gòu)建Android的gradle插件,所以我們在Android Studio中新建的每一個(gè)項(xiàng)目中都會在Project的build.gradle中都會引入構(gòu)建Android項(xiàng)目的插件
dependencies {
classpath "com.android.tools.build:gradle:4.1.3"
}
這個(gè)Gradle插件是Google開發(fā)的gradle插件,插件中實(shí)際上是定義了一系列的Task,這些Task的執(zhí)行對應(yīng)的就是一系列的構(gòu)建操作,如build(編譯)、install(安裝)等,而構(gòu)建Android項(xiàng)目的gradle插件就定義了如build、install等Task;

除了使用Google以及其他開發(fā)好的一些插件,我們自己也可以開發(fā)Gradle插件,來幫助我們自動(dòng)完成一些任務(wù),比如多渠道打包插件,比如每次發(fā)布APK之前,可能還需要對APK進(jìn)行加固,所以也可以開發(fā)一個(gè)加固插件;這樣,引入插件之后,在Gradle窗口中,直接點(diǎn)擊對應(yīng)的Task,就會自動(dòng)加固了,非常方便;
自定義插件也是使用Groovy語言來進(jìn)行開發(fā);
二、Groovy語法
上面我們說過Groovy是一種JVM語言,可以完全使用Java語法編寫代碼,但是Groovy也是 有自己的語法的,我們現(xiàn)在來學(xué)習(xí)一下Groovy的基本語法;
(1)Groovy中變量和方法的聲明
Groovy中通過def關(guān)鍵字來聲明變量和方法
def a=1;
def b="hello world";
def int c=1;
def test(){ //定義方法
println("123");
return 0;
}
Groovy寫法相對來說比較隨意,很多東西都是可以省略的;
語句結(jié)尾的分號可以省略;
定義變量和方法時(shí),變量的類型,和方法的返回值也可以省略;
Groovy中類型是弱化的,類型是可以動(dòng)態(tài)推斷的;
Groovy中很多東西都是可以省略的:
方法調(diào)用時(shí),括號也是可以省略的
方法的返回值,return都是可以省略的
def a=1;
a="Hello World"
def test(a){
println(a);
1; //方法的返回值為1,返回值可以省略return
}
test 1 //調(diào)用方法,可以省略括號
(2) Groovy中的數(shù)據(jù)類型
基本數(shù)據(jù)類型
Java中的基本數(shù)據(jù)類型,Groovy中都是有的;
但是不同的是,Groovy中的基本數(shù)據(jù)類型都是以對象的類型存在的;
def int a=1;
def double b=1.1;
println(a.class);
println(b.class);
對象類型
和Java中的對象類型是一樣的
這里把String類型單獨(dú)拿出來說下,因?yàn)镚roovy中字符串的拼接比較有特色的,可以在字符串中使用 ${變量},來拼接變量的值
def a=1;
String b="Hello ${a}" //字符串結(jié)果為 Hello 1
(3) 閉包Closure
Groovy中的閉包是一個(gè)開放的、匿名的代碼塊,可以接受參數(shù),返回值并分配給變量
{ [closureParameters] -> statements }
//定義閉包
def closure = { println("Hello World") }
def closure1= { a,b -> a+b }
//調(diào)用閉包
closure();
closure.call(); //函數(shù)式調(diào)用或者使用call方式調(diào)用,這兩種方式調(diào)用閉包都是可以的
def a=closure1(1,2);
def a=closure1.call(1,2);
(4) Grovvy中的數(shù)據(jù)結(jié)構(gòu)
前面我們說過Java中的東西,Grovvy中都是可以使用,所以Java中的數(shù)據(jù)結(jié)構(gòu),Grovvy中當(dāng)然都是可以使用的;
這里主要學(xué)習(xí)下Grovvy中和Java中不同的常用語法:
List集合
//先寫一個(gè)Java中的語法
List list=new ArrayList<>();
list.add(1);
list.add(2);
//Groovy除了像Java中一樣寫法,還可以這樣來定義List
def lists=[1,2,3];
lists.add(4);
lists.add(5);
print(lists.class); //我們打印這個(gè)lists的class,它就是java.util.ArrayList
當(dāng)然既然它就是Java中的ArrayList,當(dāng)然還有remove,get等等API,這里就不一一列出來了,但是Groovy中List還提供了find查找的API,這個(gè)是Java中所沒有的
def result=lists.find{
return it%2==0;
} //這里find查找方法傳入的參數(shù)是一個(gè)閉包,返回的是第一個(gè)能整除2的元素
def results=lists.findAll {
return it%2==0;
} //查找List中所有的能整除2的元素
def resList=lists.sort {
x1,x2-> return x1==x2?0:x1>x2?1:-1
} //List中sort方法,傳入一個(gè)閉包參數(shù),用于對List排序
lists.each{
print(it+",");
} //each方法遍歷List
List還有一些其他方法,這里可不一一列舉了
Map
//先寫一個(gè)Java中Map的寫法
Map map=new HashMap<>();
map.put("a",1);
map.put("b",2);
//Groovy除了像Java中一樣寫法,還可以這樣來定義Map
def map=["a":1,"b":2];
prinlt(map.get("a")); //獲取某個(gè)鍵對應(yīng)的值
prinltn(map.a);//map.key 這種方式也可以獲取map中某個(gè)鍵的值
println(map.getClass()); //這樣定義map,Map的默認(rèn)數(shù)據(jù)類型是java.util.LinkedHashMap
//這里我們是使用map.getClass()來獲取map的類,而不能map.class來獲取
//是因?yàn)閙ap有map.key這種用法,用來獲取map中鍵為key的值,所以如果寫map.class
//會被認(rèn)為是獲取map中鍵為class的值,所以這里只能用map.getClass()來獲取map的class類型
def v=map.find {
if (it.getValue()==2){
return it;
}
} //map中的find方法,傳入一個(gè)閉包參數(shù),可以查找map中第一個(gè)滿足條件的一個(gè)Entry(鍵值對實(shí)體)
println(v.key);
map.each {
println(it.getKey()+":"+it.getValue());
} //可以使用map的each方法,參數(shù)是一個(gè)閉包,來遍歷map
map還有一些其他方法,所以這里可不一一列舉了
def resMap=map.sort {
e1,e2->
return e1.key==e2.key?0:e1.key>e2.key?1:-1
} //map的sort方法傳入一個(gè)閉包參數(shù),對map進(jìn)行排序,返回的是一個(gè)排序后的新的map,注意原map并沒有變化
Range (范圍)
rang相當(dāng)于一個(gè)輕量級的List
def rang=1..10 //這里相當(dāng)于定義了一個(gè)集合,集合中的元素值從1到10
println(range.class); //groovy.lang.IntRange
println(rang[0]) //range[0]為1
println(range.getFrom());// 1 range.getFrom() 獲取range的下限值
println(range.getTo()); //10 range.getTo()獲取range的上限值
//如果定義的def range=10..1; range.getFrom()和range.getTo()的值還是1,10
//因?yàn)?0..1 下限值還是1,上限值還是10
def charRange='f'..'a';
charRange.each {
println(it)
} //打印出 f,e,d,c,b,a
def score=60;
switch (score){
case 0..59:
println("及格");
break
case 60..85:
println("及格");
break
case 86..100:
println("優(yōu)秀");
break
}
(5)Groovy中的class(類)、inteface(接口)、trait
class
Groovy中的class(類)和Java中的class(類)是一樣的,唯一的區(qū)別在于
默認(rèn)的類、成員、方法的訪問權(quán)限都是public的
interface
Groovy中的interface(接口)和Java中的interface(接口)是一樣的,區(qū)別可能還是Groovy中定義接口的時(shí)候,如果不寫訪問權(quán)限默認(rèn)就是public的而Java中定義接口的時(shí)候,如果不寫訪問權(quán)限,默認(rèn)不是public的,那么不同包下的類是無法訪問這個(gè)接口的;
trait
trait這個(gè)是Groovy中新加的類型,但是其實(shí)他就是一種介于抽象類和接口之間的類型;trait中可以定義抽象方法(即不實(shí)現(xiàn)),也可以定義正常方法; 使用的時(shí)候,和接口一樣,也是要implement的,非抽象類實(shí)現(xiàn)是需要實(shí)現(xiàn)其抽象方法
trait MyTrait{
abstract void test();
void play(){
prinlt("play game");
}
}
class TestClass implement MyTrait{
@Override
void test(){
}
}
三、自定義Gradle插件
自定義插件的方式:
1.獨(dú)立項(xiàng)目
一個(gè)獨(dú)立的Java項(xiàng)目/模塊,可以將文件包發(fā)布到倉庫(jcenter),使其他項(xiàng)目方便引用
2.Build script腳本
把插件寫在build.gradle文件中,一般用于簡單的邏輯,只在該build.gradle中可見
3.buildSrc目錄
將插件源碼放在buildSrc/src/main/groovy/中,只對該項(xiàng)目可見
我們這里以獨(dú)立的Java Library模塊自定義插件方式為例,來學(xué)習(xí)一下如何自定義一個(gè)Gradle插件;
每次發(fā)布APK之前,可能還需要對APK進(jìn)行加固,所以也可以開發(fā)一個(gè)加固插件,來自動(dòng)進(jìn)行APK加固,我們這里以開發(fā)一個(gè)360加固插件為例;
我們知道如果是手動(dòng)使用360加固,我們肯定是需要打開360加固的軟件,然后輸入用戶名,密碼登錄進(jìn)軟件,然后選擇應(yīng)用加固選項(xiàng),上傳打包好的APK文件,就會自動(dòng)進(jìn)行加固,加固好之后,就會自定下載加固好的APK,然后我們還需要對加固好的APK文件進(jìn)行簽名,簽名后的加固APK文件就可以正常安裝使用了;



如果我們是使用自定義插件的方式來自動(dòng)完成360加固,那上面這些步驟,從登錄開始到上傳應(yīng)用,到加固,到對加固后的應(yīng)用進(jìn)行簽名,都要以代碼的方式來實(shí)現(xiàn);
(1)我們使用Android Studio來創(chuàng)建一個(gè)正常的Android項(xiàng)目,在Android項(xiàng)目中新建一個(gè)Java Library Module;
Java Library Module名字,這里我們?nèi)lugin
Module的build.gradle文件中引入groovy插件,dependencies中
引入gradle插件,以及gradleApi因?yàn)樵诰帉懖寮臅r(shí)候,是要用到gradle相關(guān)的API的
plugins { id 'groovy' } 或者 apply plugin:'groovy'
dependencies {
implementation 'com.android.tools.build:gradle:4.1.3'
implementation gradleApi()
}
(2) Android Studio 切換項(xiàng)目試圖為Project,然后修改plugin的src/main/java 這個(gè)java目錄名字為groovy

(3) 然后在src/main/groovy/…(包名)/下新建插件源碼文件
這里要新建File(注意這里必須要新建一個(gè)File,不是新建java ,class等),File文件的名字以.groovy后綴結(jié)尾
我們這里新建一個(gè)GiaGuTask.groovy 的File,然后在GiaGuTask.groovy中開始插件的開發(fā):首先要先添加包名,由于 Android Stuido不能識別.groovy文件 所以不會給我們自動(dòng)添加包名,所以需要我們自己添加包名;package com.example.plugin
這個(gè)Task中就是將上述的登錄360加固軟件,上傳APK,加固,簽名加固后的APK文件,全部用執(zhí)行命令的方式來實(shí)現(xiàn),360加固軟件包下的help.txt中實(shí)際上就提供了上 登錄360加固軟件,上傳APK,加固,簽名加固后的APK文件的命令行命令


所以我們的新建的Task如下:
package com.example.plugin
import com.android.builder.model.SigningConfig
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
class GiaGuTask extends DefaultTask{
GiaGuBean mGiaGuBean;
SigningConfig signingConfig;
File apkFile;
GiaGuTask() {
group="jiagu"
}
@TaskAction
void run() {
project.exec {
it.commandLine(
"java",
"-jar",
mGiaGuBean.getJiaGuTools(),
"-login",
mGiaGuBean.getUserName(),
mGiaGuBean.getPassword(),
);
}
if (signingConfig) {
project.exec {
it.commandLine(
"java",
"-jar",
mGiaGuBean.getJiaGuTools(),
"-importsign",
signingConfig.storeFile.absolutePath,
signingConfig.storePassword,
signingConfig.keyAlias,
signingConfig.keyPassword
)
}
}
project.exec {
it.commandLine("java",
"-jar",
mGiaGuBean.getJiaGuTools(),
"-jiagu",
apkFile.absolutePath,
apkFile.parent,
"-autosign"
)
}
}
}
當(dāng)然這里的GiaGuTask使用到了一個(gè)GiaGuBean,這個(gè)GiaGuBean里面定義了一些我們需要到時(shí)候在build.gradle
設(shè)置的配置信息,登錄的用戶名,密碼,加固工具所在的位置(加固工具就是360加固軟件包下的jiagu.jar文件)

這些需要從build.gradle中設(shè)置的配置信息,我們需要在GiaGuBean這個(gè)實(shí)體類中定義,并且定義set,get方法
package com.example.plugin
class GiaGuBean {
String userName;
String password;
String jiaGuTools
String getUserName() {
return userName
}
void setUserName(String userName) {
this.userName = userName
}
String getPassword() {
return password
}
void setPassword(String password) {
this.password = password
}
String getJiaGuTools() {
return jiaGuTools
}
void setJiaGuTools(String jiaGuTools) {
this.jiaGuTools = jiaGuTools
}
}
然后新建一個(gè)GiaGuPlugin.groovy 的File,然后定義一個(gè)class GiaGuPlugin 需要實(shí)現(xiàn) DefaultPlugin
并且實(shí)現(xiàn)apply方法,在apply方法中進(jìn)行相應(yīng)的實(shí)現(xiàn),并且創(chuàng)建一個(gè)或者多個(gè)Task的,因?yàn)樽罱K的插件功能,都是一個(gè)個(gè)Task的執(zhí)行
package com.example.plugin
import com.android.build.gradle.AppExtension
import com.android.build.gradle.api.ApplicationVariant
import com.android.build.gradle.api.BaseVariantOutput
import com.android.builder.model.SigningConfig
import org.gradle.api.Plugin
import org.gradle.api.Project
class GiaGuPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
GiaGuBean giaGuBean = project.extensions.create("jiagu", GiaGuBean.class);
project.afterEvaluate {
AppExtension appExtension = project.extensions.android;
appExtension.applicationVariants.all {
//debug/release
ApplicationVariant variant ->
//對應(yīng)變體(debug/release)的簽名配置
SigningConfig signingConfig = variant.signingConfig;
variant.outputs.all {
BaseVariantOutput baseVariantOutput ->
//輸出的APK文件
File apkFile = baseVariantOutput.outputFile;
//創(chuàng)建加固任務(wù)
GiaGuTask giaGuTask = project.tasks.create("jiagu${variant.baseName.capitalize()}", GiaGuTask.class);
giaGuTask.mGiaGuBean = giaGuBean;
giaGuTask.signingConfig = signingConfig;
giaGuTask.apkFile =apkFile;
}
}
}
(4)插件的配置
插件開發(fā)好了之后,就需要進(jìn)行配置,plugin Module的src/main目錄下新建一個(gè)resources目錄,再在resources目錄下新建一個(gè)META-INF目錄,再在META-INF目錄下新建一個(gè)gradle-plugins目錄; 然后在gradle-plugins目錄下新建一個(gè)以 .properties后綴的文件,這個(gè)文件的名字就是到時(shí)候我們在引用插件時(shí)的名字我們這里取名:com.example.plugin.properties;這個(gè)文件里面我們需要配置插件的實(shí)現(xiàn)類
implementation-class=com.example.plugin.GiaGuPlugin
到時(shí)候我們引用插件的時(shí)候就使用com.example.plugin這個(gè)名字;

(5)打包插件上傳倉庫
我們這里是打包插件上傳Maven本地倉庫,需要依賴maven-plugin插件,所以在plugin這個(gè)Java Library Module的build.gradle中添加maven-plugin插件的使用
apply plugin:'maven-publish'
publishing{
publications{
Jiagu(MavenPublication){ //Jiagu 這個(gè)名字隨意取
from components.java //這個(gè)表示要把源碼生成的jar包上傳
groupId 'com.example' //配置插件的groupId(分組ID)
artifactId "plugin" //配置ID
version "1.0" //配置版本號
}
}
}
groupId、artifactId、version這三個(gè)配置構(gòu)成了到時(shí)候我們依賴插件的時(shí)候的寫法: groupId:artifactId:version,我
們到時(shí)候依賴這個(gè)plugin插件的時(shí)候,應(yīng)該是在Android項(xiàng)目的Project的build.gradle的dependencies中添加依賴:classpath “com.example:plugin:1.0”
引用maven-publish插件之后,build(編譯) plugin module之后,我們就可以在gradle窗口中查看到plugin下Tasks下會有publishing的Task,點(diǎn)開publishing這個(gè)Task下,我們點(diǎn)擊publishToMavenLocal(打包上傳插件到本地的maven庫),操作成功過后,在C:\Users\Administrator\下回有一個(gè).m2目錄,這個(gè)目錄的repository目錄下會有 com\example\plugin這個(gè)目錄所以是1.0(version)目錄下就會有對應(yīng)的插件plugin-1.0.jar,和兩個(gè)插件信息文件:plugin-1.0.module、plugin-1.0.pom;


(5)使用插件
添加插件的依賴
我們在Android項(xiàng)目中的Project的build.gradle中添加插件依賴 由于我們使用的是本地maven庫中的插件,所以repositories中需要添加mavenLocal()倉庫
repositories {
...
mavenLocal()
}
dependencies {
...
classpath "com.example:plugin:1.0"
//就是之前上傳的是偶配置groupId:artifactId:version
}
引用插件
在Android項(xiàng)目主module(一般為app)的build.gradle中引用插件
apply plugins:'com.example.plugin' //就是開發(fā)插件的時(shí)候resources/META-INF/gradle-plugins/com.example.plugin.properties 這個(gè)文件的名字
或者現(xiàn)在都是這種寫法,在plugins{}下添加插件ID
plugins {
...
id 'com.example.plugin'
//就是開發(fā)插件的時(shí)候resources/META-INF/gradle-plugins/com.example.plugin.properties 這個(gè)文件的名字
}
當(dāng)然app主module中的build.gradle中還要配置之前在定義GiaGuBean中的登錄用戶名,密碼,加固工具位置信息

執(zhí)行插件任務(wù)
上述步驟之后,就會在gradle窗口中主module(app)的Tasks下會有一個(gè)jiagu任務(wù),這個(gè)名字實(shí)際就是你開發(fā)插件的 時(shí)候,定義Task時(shí)候,為Task設(shè)置的groupId,如果沒有設(shè)置task的groupId,則插件的任務(wù)會在module(app)Tasks的other目錄下點(diǎn)擊jiagu下的操作,就會執(zhí)行我們的加固任務(wù);
當(dāng)然我們要先確保APK文件已經(jīng)存在,因?yàn)橹拔覀兌xGiaGuPlugin.groovy時(shí),里面APK文件使用的是baseVariantOutput.outputFile,也即build/outputs/apk/下release或者debug目錄下的APK,所以我們只要build一下就會生成APK文件,然后我們再點(diǎn)擊gradle窗口下,app/Tasks/jiagu下的加固任務(wù)即可進(jìn)行加固

整個(gè)加固的過程,在run窗口下都會有日志打印



加固任務(wù)執(zhí)行完畢之后build/outputs/apk/ 對用的目錄下就會生成加固簽名后的APK文件

以上就是一個(gè)自定義gradle插件的,實(shí)現(xiàn)、上傳、使用過程
到此這篇關(guān)于Android自定義Gradle插件的文章就介紹到這了,更多相關(guān)Android自定義Gradle插件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 為Android Studio編寫自定義Gradle插件的教程
- 詳解如何使用Android Studio開發(fā)Gradle插件
- 詳解Android Gradle插件3.0挖坑日記
- Android gradle插件打印時(shí)間戳的方法詳解
- Android Studio Gradle插件版本與Gradle版本之間的對應(yīng)關(guān)系
- AndroidStudio升級4.1坑(無法啟動(dòng)、插件plugin不好用、代碼不高亮)
- AndroidStudio升級4.1后啟動(dòng)失敗Plugin問題解決
- 解決Android Studio4.1沒有Gsonfomat插件,Plugin “GsonFormat” is incompatible的問題
- Android?Studio?中Gradle配置sonarqube插件(推薦)
- Android?Gradle?插件自定義Plugin實(shí)現(xiàn)注意事項(xiàng)
相關(guān)文章
Android_RecyclerView實(shí)現(xiàn)上下滾動(dòng)廣告條實(shí)例(帶圖片)
本篇文章主要介紹了Android_RecyclerView實(shí)現(xiàn)上下滾動(dòng)廣告條實(shí)例(帶圖片),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
Android使用音頻信息繪制動(dòng)態(tài)波紋
這篇文章主要介紹了Android使用音頻信息繪制動(dòng)態(tài)波紋 的相關(guān)資料,需要的朋友可以參考下2016-02-02
Android基于API的Tabs3實(shí)現(xiàn)仿優(yōu)酷t(yī)abhost效果實(shí)例
這篇文章主要介紹了Android基于API的Tabs3實(shí)現(xiàn)仿優(yōu)酷t(yī)abhost效果,結(jié)合完整實(shí)例形式分析了Android實(shí)現(xiàn)優(yōu)酷界面效果的相關(guān)技巧,需要的朋友可以參考下2015-12-12
詳解Flutter手游操縱桿移動(dòng)的原理與實(shí)現(xiàn)
這篇文章將為大家詳細(xì)介紹一下Android?Flutter手游中操縱桿移動(dòng)角色的原理與實(shí)現(xiàn)過程,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-07-07
android實(shí)現(xiàn)session保持簡要概述及實(shí)現(xiàn)
其實(shí)sesion在瀏覽器和web服務(wù)器直接是通過一個(gè)叫做name為sessionid的cookie來傳遞的,所以只要在每次數(shù)據(jù)請求時(shí)保持sessionid是同一個(gè)不變就可以用到web的session了,感興趣的你可以參考下本文或許對你有所幫助2013-03-03
一個(gè)簡單的toolabar結(jié)合drawlayout使用方法
這篇文章主要為大家詳細(xì)介紹了一個(gè)簡單的toolabar結(jié)合drawlayout的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
android換膚功能 如何動(dòng)態(tài)獲取控件中背景圖片的資源id?
這篇文章主要為大家詳細(xì)介紹了android換膚功能中如何動(dòng)態(tài)獲取控件中背景圖片的資源id? ,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-08-08
接口對象的實(shí)例化在接口回調(diào)中的使用方法
下面小編就為大家?guī)硪黄涌趯ο蟮膶?shí)例化在接口回調(diào)中的使用方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02
Android學(xué)習(xí)筆記——Menu介紹(三)
今天繼續(xù)昨天沒有講完的Menu的學(xué)習(xí),主要是Popup Menu的學(xué)習(xí),需要的朋友可以參考下2014-10-10

