Skip to content

Commit

Permalink
feature: skin maker plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
cgspine committed Oct 10, 2019
1 parent 1ca5622 commit 6e19018
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 5 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

buildscript {
repositories {
mavenLocal()
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'
classpath 'com.qmuiteam:skin-maker-plugin:0.0.1'
}
}

Expand Down
3 changes: 3 additions & 0 deletions qmuidemo-skin-code-generator-source
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
com.qmuiteam.qmuidemo.fragment.lab.QDArchTestFragment
QDArchTestFragment,$0.mTitleTv.setTag(com.qmuiteam.qmui.R.id.qmui_skin_value, "background:app_skin_common_background")
;
11 changes: 7 additions & 4 deletions qmuidemo/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,13 @@ android {
}
}

//apply plugin: 'com.qmuiteam.qmui'
//qmui {
// parentTheme "AppRootTheme"
//}

apply plugin: 'com.qmuiteam.qmui.skinMaker'
skinMaker{
file rootProject.file('qmuidemo-skin-code-generator-source')
}



// 加@aar与不加@aar的区别:
// http://stackoverflow.com/questions/30157575/why-should-i-include-a-gradle-dependency-as-aar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.qmuiteam.qmuidemo.base.BaseFragment;
import com.qmuiteam.qmuidemo.lib.annotation.Widget;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.ButterKnife;
Expand All @@ -59,6 +60,10 @@ public class QDArchTestFragment extends BaseFragment {
@BindView(R.id.btn) QMUIRoundButton mBtn;
@BindView(R.id.btn_1) QMUIRoundButton mBtn1;

@Override
protected void onViewCreated(@NonNull View rootView) {
super.onViewCreated(rootView);
}

@Override
protected View onCreateView() {
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
include ':qmuidemo', ':qmui', ':lib', ':compiler', ':lint', ':lintrule', ':arch', ':arch-compiler',
':arch-annotation', ':skin-maker'
':arch-annotation', ':skin-maker', ':skin-maker-plugin'
1 change: 1 addition & 0 deletions skin-maker-plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
39 changes: 39 additions & 0 deletions skin-maker-plugin/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
plugins {
id 'java-gradle-plugin'
id 'groovy'
id "com.gradle.plugin-publish" version "0.10.1"
}

version = QMUI_SKIN_MAKER_VERSION

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation gradleApi()
implementation localGroovy()

implementation 'org.javassist:javassist:3.18.2-GA'
implementation 'com.android.tools.build:gradle-api:3.5.1'
implementation 'com.android.tools.build:gradle:3.5.1'
implementation 'com.squareup:javapoet:1.10.0'
implementation 'commons-io:commons-io:2.6'

testImplementation 'junit:junit:4.12'
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

gradlePlugin {
plugins {
skinMakerPlugin {
id = 'com.qmuiteam.qmui.skinMaker'
implementationClass = 'com.qmuiteam.qmui.SkinMakerPlugin'
}
}
}


File deployConfig = rootProject.file('gradle/deploy.properties')
if (deployConfig.exists()) {
apply from: rootProject.file('gradle/deploy.gradle')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.qmuiteam.qmui

import org.gradle.api.Plugin
import org.gradle.api.Project

class SkinMakerPlugin implements Plugin<Project> {

@Override
void apply(Project target) {
def extension = target.extensions.create("skinMaker", SkinMaker.class)
target.android.registerTransform(new SkinMakerTransform(target, extension))
}

static class SkinMaker {
File file
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.qmuiteam.qmui

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
import org.gradle.api.logging.LogLevel

class SkinMakerTransform extends Transform {

private Project mProject
private SkinMakerPlugin.SkinMaker mSkinMaker

SkinMakerTransform(Project project, SkinMakerPlugin.SkinMaker skinMaker) {
mProject = project
mSkinMaker = skinMaker
}


@Override
String getName() {
return "skin-maker"
}

@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}

@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}

@Override
boolean isIncremental() {
return false
}

@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
File sourceFile = mSkinMaker.file
if (sourceFile == null || !sourceFile.exists()) {
return
}
mProject.logger.log(LogLevel.INFO, "skin code source: " + sourceFile.path)

def injectCode = new InjectCode()
injectCode.parseFile(sourceFile)

def androidJar = mProject.android.bootClasspath[0].toString()

def externalDepsJars = new ArrayList<File>()
def externalDepsDirs = new ArrayList<File>()

transformInvocation.referencedInputs.forEach { transformInput ->
externalDepsJars += transformInput.jarInputs.map { it.file }
externalDepsDirs += transformInput.directoryInputs.map { it.file }
}

transformInvocation.outputProvider.deleteAll()

transformInvocation.inputs.each { input ->
input.directoryInputs.each { directoryInput ->

def baseDir = directoryInput.file
ClassPool pool = new ClassPool()
pool.appendSystemPath()
pool.appendClassPath(baseDir.absolutePath)
pool.appendClassPath(androidJar)
externalDepsJars.each { pool.insertClassPath(it.absolutePath) }
externalDepsDirs.each { pool.insertClassPath(it.absolutePath) }


directoryInput.file.eachFileRecurse { file ->
String filePath = file.absolutePath
if (filePath.endsWith(".class")) {
def className = filePath.substring(directoryInput.file.absolutePath.length() + 1, filePath.length() - 6)
.replace('/', '.')
def codes = injectCode.getCode(className)
if (codes != null && !codes.isEmpty()) {
CtClass ctClass = pool.getCtClass(className)
if (ctClass.isFrozen()) {
ctClass.defrost()
}
codes.keySet().each { prefix ->
def sb = new StringBuilder()
sb.append("public void skinMaker")
sb.append(prefix)
sb.append("(){")
codes.get(prefix).each { text ->
sb.append(text)
sb.append(";")
}
sb.append("}")
CtMethod newMethod = CtMethod.make(sb.toString(), ctClass)
ctClass.addMethod(newMethod)
}

if (className.endsWith("Fragment")) {
CtMethod ctMethod = ctClass.getDeclaredMethod("onViewCreated", pool.get("android.view.View"))
ctMethod.insertAfter("skinMaker" + className.split("\\.").last() + "();")
} else if (className.endsWith("Activity")) {
CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate", pool.get("android.os.Bundle"))
ctMethod.insertAfter("skinMaker" + className.split("\\.").last() + "();")
}

ctClass.writeFile(baseDir.absolutePath)
ctClass.detach()
}
}
}

def dest = transformInvocation.outputProvider.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY)

FileUtils.copyDirectory(directoryInput.file, dest)
}

//遍历jar文件 对jar不操作,但是要输出到out路径
input.jarInputs.each { jarInput ->
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = transformInvocation.outputProvider.getContentLocation(
jarName + md5Name,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
}


class InjectCode {
private HashMap<String, HashMap<String, ArrayList<String>>> mCodeMap = new HashMap<>()

private String mCurrentClassName = null
private HashMap<String, ArrayList<String>> mCurrentCodes = null

void parseFile(File file) {
file.newReader().lines().each { text ->
if (text != null) {
text = text.trim()
if (!text.isBlank()) {
if (mCurrentClassName == null) {
mCurrentClassName = text
mCurrentCodes = new HashMap<String, ArrayList<String>>()
mCodeMap.put(mCurrentClassName, mCurrentCodes)
} else if (text != ";") {
int split = text.indexOf(",")
if (split > 0 && split < text.length()) {
String key = text.substring(0, split)
ArrayList<String> codes = mCurrentCodes.get(key)
if (codes == null) {
codes = new ArrayList<String>()
mCurrentCodes.put(key, codes)
}
codes.add(text.substring(split + 1, text.length()))
}
} else {
mCurrentClassName = null
mCurrentCodes = null
}
}
}
}
}

HashMap<String, ArrayList<String>> getCode(String className) {
return mCodeMap.get(className)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.qmuiteam.qmui.skinMaker;

import org.junit.Test;

import static org.junit.Assert.*;

/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

0 comments on commit 6e19018

Please sign in to comment.