Android开发组件化的一些思考

作者:CodeOver

前言

组件化开发现在基本上属于基础操作了,大家一般都会使用 ARouter 、LiveDataBus 作为组件化通信的解决方案,那为什么会选择ARouter,ARouter又是怎么实现的呢?这篇文章主要就 搭建组件化开发的准备工作 、组件化跳转分析,如果理解了这篇文章,对于查看ARouter源码应该是会有很大的帮助的。至于ARouter等分析,网上有很多的讲解,这里就不分析ARouter源码了,文章末尾会给出ARouter源码时序图和总结,可忽略。

ps: 为什么写本文,因为笔者最近被问道,为什么要用ARouter,ARouter它到底是解决什么问题,你能就一个点来分析吗?被问到该问题了?笔者是从它的跳转回答的,毕竟跳转简单。刚好记录并回忆一下。

目录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-25F73yKH-1656943411914)(https://upload-images.jianshu.io/upload_images/16810022-95ce1c67ee020ae7.jpg_14">一、组件化优势

组件化的优势想必大家都知道,可以总结为四点

  • 编译速度 我们可以按需求测试单一业务模块,极大的提升了我们的开发速度
  • 解耦 极度的降低了模块之间的耦合,便于后期维护与更新,当产品提出一个新业务时,完全可以新建一个业务组件,集成和摒弃都很方便
  • 功能重用 某一块的功能在另外的组件化项目中使用只需要单独依赖这一模块即可
  • 团队开发效率 组件化架构是团队开发必然会选择的一种开发方式,它能有效的使团队更好的协作

二、组件化开发准备工作

组件化开发,一般可以分为三层,分别为 壳工程、业务组件、基础依赖库,业务组件间互不关联,并且业务组件需要可以单独运行测试,整体都是围绕解耦来开展的,下面开始进行组件化开发前所需要做的准备工作

1、包名和资源文件命名冲突问题

需要制定规范,对包名和项目模块的划分规范化,不同模块内不能有相同名字的类文件,避免打包失败等冲突问题

2、Gradle中的版本好统一管理

在我们创建的模块中,有一些,例如 compileSdkVersion 、buildToolsVersion 或者是集成的第三方依赖库,它们都有对应的版本号,如果不进行统一管理,后续维护很麻烦,总不能对所有模块一个个手动修改版本。所以我们可以在gradle.properties文件中,添加配置,例如

gradle.properties
CompileSdkVersion = 30// 这里不能和compileSdkVersion 一样,会报错
​
模块的build.gradle
android{
    compileSdkVersion CompileSdkVersion.toInteger()
}

所有模块版本号都按照上面的写,每次改版本号都按照gradle.properties里面定义的修改就好。但是,细心的你一定会发现,现在网上的例子,这些写的很少,既然这样写也能做到统一管理,为什么不推荐呢?答案就在 CompileSdkVersion.toInteger() 这里,这里拿到CompileSdkVersion后还需要转换,如果使用下面创建gradle文件的做法,完全可以省去。
在项目根目录下新建一个**conffig.gradle **文件,和全局build.gradle同一层级

config.gradle
​
ext{
    android=[
            compileSdkVersion:29,buildToolsVersion:'29.0.2',targetSdkVersion:29,]
    dependencies  = [
         appCompact : 'androidx.appcompat:appcompat:1.0.2'
    ]
}
根目录的build.gradle中,顶部加入
apply from:"config.gradle"
​
使用的时候如下
compileSdkVersion rootProject.ext.android.compileSdkVersion
implementation rootProject.ext.dependencies.appCompact

注意,在implementation dependencies 时候是可以这样写的

implementation 'androidx.test.ext:junit:1.1.0','androidx.test.espresso:espresso-core:3.1.1'

但是你在config.gradle中千万不能也类似这样写

 dependencies  = [
         appCompact : '\'androidx.appcompat:appcompat:1.0.2\',\'androidx.test.espresso:espresso-core:3.1.1\''
    ]

因为在build.gradle中你把所有依赖放到implementation后面,用逗号分隔,这个逗号和字符串的逗号不一样,你在config.gradle中那样写的其实相当于在build.gradle implementation dependencies 时这样写

implementation 'androidx.test.ext:junit:1.1.0,androidx.test.espresso:espresso-core:3.1.1'

那你可能会问,这样写不行的话,那我怎么在config.gradle中实现对所有模块需要的公共依赖库集中管理呢?可以按照下面这样写

ext {
    ....
    dependencies = [
            publicImplementation: [
                    'androidx.test.ext:junit:1.1.0','androidx.test.espresso:espresso-core:3.1.1'
            ],appCompact          : 'androidx.appcompat:appcompat:1.0.2'
    ]
}
​
implementation rootProject.ext.dependencies.publicImplementation //每个模块都写上这句话就好了

这样就完了吗?还有我们自己写的的公共库也要集中管理,一般我们都会在模块的build.gradle中一个个这样写

implementation project(path: ':basic')

现在我们通过gradle来管理,如下

ext {
    ....
    dependencies = [
            other:[
                ':basic',]
    ]
}
​
rootProject.ext.dependencies.other.each{
    implementation project(it)
}

3、组件在Application和Library之间随意切换

Library不能在Gradle文件中有applicationId
AndroidManifest.xml文件区分
在开发过程中,需要独立测试,避免不了经常在Application和Library之间随意切换。在模块,包括壳工程app模块运行时,Application类只能有一个。
首先我们在config.gradle中配置,为什么不在gradle.properties中配置,之前也说了

ext {
    android = [
            compileSdkVersion: 29,buildToolsVersion: '29.0.2',targetSdkVersion : 29,isApplication:false,]
....
}

然后在各个模块的build.gradle文件顶部加入以下判断

if(rootProject.ext.android.isApplication) {
    apply plugin: 'com.android.application'
}else{
    apply plugin: 'com.android.library'
}
  • Library不能在Gradle文件中有applicationId
android {
    defaultConfig {
        if(rootProject.ext.android.isApplication){
            applicationId "com.cov.moduletest" //作为依赖库,是不能有applicationId的
        }
        ....
 }
  • 在app模块的gradle中也需要有区分
 dependencies {
         .....
    if(!rootProject.ext.android.isApplication){
        implementation project(path: ':customer')  //只有当业务模块是依赖的时候去依赖 ,看业务需求
    }
  }
  • AndroidManifest.xml文件区分
    在各个模块的build.gradle中区分
    sourceSets {
        main{
            if(rootProject.ext.android.isApplication){
                manifest.srcFile '/src/main/AndroidManifest.xml'
            }else{
                manifest.srcFile "src/main/manifest/AndroidManifest.xml"
            }
        }
    }
  • Application配置

因为我们会在Application中做一些初始化操作,如果模块单独运行的话,那么这些操作需要放到模块的Application中,所以这里需要单独配置一下,新建module 文件夹,配置好下面文件时,新建自定义的Application类,然后在manifest文件夹下的清单文件内指定Application。这样作为依赖库运行时,module 文件夹下的文件不会进行编译。

main{
     if(rootProject.ext.android.isApplication){
   manifest.srcFile '/src/main/AndroidManifest.xml'
       }else{
      manifest.srcFile "src/main/manifest/AndroidManifest.xml"
   java.srcDirs 'src/main/module','src/main/java'
  }
 }

4、在Java代码中判断是否独立运行还是集成运行

在运行时,每个模块都会生成一个对应的BuildConfig类,存放包路径可能不同,那我们怎么做呢?
在basic模块的build.gradle中加入以下代码

buildTypes {
        release {
            buildConfigField 'boolean','isApplication',rootProject.ext.android.isApplication.toString()
        }
        debug {
            buildConfigField 'boolean',rootProject.ext.android.isApplication.toString()
        }
}

为什么要在basic模块下加入呢?就是因为BuildConfig每个模块都会有,总不能在所有模块都加入这句话吧。在basic模块加入后,其它模块依赖这个模块,然后通过在basic模块中定义的BaseActivity中,添加获取该值的方法即可,其他模块继承BaseActivity,就可以拿到父类方法进行判断了,这只是一种,具体要看业务进行分析。

三、组件化跳转分析

1、自定义组件化跳转模块

按照上述配置后,接下啦第一步就需要解决组件化通信问题,其中第一类问题就是跳转相关。因为业务组件之间不能耦合,所以我们只能通过自定义一个新的 router 模块,各个业务组件内通过继承该依赖,然后实现跳转。

我们只需要在router模块中定义一个ARouter容器类,然后各个模块进行注册Activity,就可以使用了,代码如下

public class ARouter {
    private static ARouter aRouter = new ARouter();
    private HashMap<String,Class<? extends Activity>> map = new HashMap<>();
    private Context mContext;
​
    private ARouter(){
    }
    public static ARouter getInstance(){
        return aRouter;
    }
​
    public void init(Context context){
        this.mContext = context;
    }
    /**
     * 将类对象添加到容器中
     * @param key
     * @param clazz
     */
    public void registerActivity(String key,Class<?extends  Activity> clazz){
        if(key != null && clazz != null && !map.containsKey(key)){
            map.put(key,clazz);
        }
    }
    public void navigation(String key){
        navigation(key,null);
    }
​
    public void navigation(String key,Bundle bundle){
        if(mContext == null){
            return;
        }
        Class<?extends  Activity > clazz = map.get(key);
        if(clazz != null){
            Intent intent = new Intent(mContext,clazz);
            if(bundle != null){
                intent.putExtras(bundle);
            }
            mContext.startActivity(intent);
        }
    }
}

通过ARouter.getInstance().navigation(“key”) 就能跳转了,但是前提是需要调用registerActivity将每个Activity和对应路径注册进来,那不可能在每个Activity中都调用该方法将类对象加到ARouter路由表吧?我们可能会想到在BasicActivity里面加一个抽象方法,将所有类对象返回,然后你拿到后调用registerActivity方法注册,但是这个前提是 需要你继承BasicActivity的类已经创建了,已经实例化了,所以这不可能在没启动Activity时进行注册。那怎么样才能在Activity没启动时,将所有类对象添加到ARouter容器内呢?有什么方法可以在Application创建时候可以收集到所有未启动的Activity呢?

可能大家还会想到,在每一个模块里面新建一个ActivityUtils类,然后定义一个方法,里面调用ARouter.registerActivity ,注册该模块所有需要注册的类,然后在Application类里触发该方法。模块少还好说,可以一个个手动敲,模块一多,每个模块都得写,维护太麻烦了,可不可以自动生成这样的方法,自动找到需要注册的类,收集起来呢?

这就需要使用APT技术来实现了,通过对需要跳转的Activity进行注解,然后在编译时生成类文件及类方法,该类方法内利用Map收集对应的注解了的类,在Application创建时,执行这些类文件相关方法,收集到ARouter容器内。

2、组件化跳转实现方案升级

不了解如何操作APT的同学可以参考
Android APT 实践
谈谈APT和JavaPoet的一些使用技巧和要点

要实现上述说的方案,需要了解一下APT(Annotation Processing Tool)技术,即注解处理器,它是Javac的一个工具,主要用来在编译时扫描和处理注解。

  • 创建注解,对需要注册的Activity类用注解标记 (annotation模块)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityPath {
    String value();
}
  • 创建注解处理器 并生成类 (annotation_compiler模块)

**@AutoService(Processor.class) **虚拟机在编译的时候,会通过这个判断AnnotationCompiler是注解处理器,是固定的写法,加个注解即可,通过auto-service中@AutoService可以自动生成AutoService注解处理器,用来注册用来生成 META-INF/services/javax.annotation.processing.Processor 文件

**@SupportedSourceVersion(SourceVersion.RELEASE_7) **指定JDK编译版本

@SupportedAnnotationTypes({Constant.ACTIVITY_PATH}) 指定注解,这里填写ActivityPath的类的全限定名称 包名.ActivityPath

Filer 对象,用来生成Java文件的工具

Element 官方解释 表示程序元素,如程序包,类或方法,TypeElement表示一个类或接口程序元素,VariableElement表示一个字段、枚举常量或构造函数参数、局部变量,TypeParameterElement表示通用类、接口、方法、或构造函数元素的正式类型参数,这里简单举个例子

package  com.example  //PackageElement
public class A{ //TypeElement
    private int a;//VariableElement
    private A mA;//VariableElement
    public A(){}  // ExecuteableElement
    public  void setA(int a){ // ExecuteableElement   参数a是VariableElement

    }

}

还需要注意一点,为了在编译时不出现GBK编码错误等问题,需要在gradle中添加

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

接下来就开始真正实现了,现在annotation_compile的依赖中添加

    implementation'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
    implementation 'com.squareup:javapoet:1.11.1'

然后实现注解处理器类

@AutoService(Processor.class)
@SupportedAnnotationTypes({Constant.ACTIVITY_PATH})
// 注解处理器接收的参数
@SupportedOptions(Constant.MODULE_NAME)
public class AnnotationCompiler extends AbstractProcessor {
​
    //生成java文件的工具
    private Filer filer;
    private String moudleName;
​
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnv.getFiler();
        moudleName = processingEnv.getOptions().get(Constant.MODULE_NAME);
    }
​
    /**
     * 得到最新的Java版本
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return processingEnv.getSourceVersion();
    }
​
    /**
     * 找注解 生成类
     *
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set,RoundEnvironment roundEnvironment) {
        if (moudleName == null) {
            return false;
        }
        //得到模块中标记了ActivityPath的注解
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ActivityPath.class);
        //存放 路径 类文件名称
        Map<String,String> map = new HashMap<>();
        //TypeElement 类节点
        for (Element element : elements) {
            TypeElement typeElement = (TypeElement) element;
            ActivityPath activityPath = typeElement.getAnnotation(ActivityPath.class);
            String key = activityPath.value();
            String activityName = typeElement.getQualifiedName().toString();//得到此类型元素的完全限定名称
            map.put(key,activityName + ".class");
        }
​
        //生成文件
        if (map.size() > 0) {
            createClassFile(map);
        }
        return false;
    }
​
    private void createClassFile(Map<String,String> map) {
        //1.创建方法
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("registerActivity")
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class);
​
        Iterator<String> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            String key = iterator.next();
            String className = map.get(key);
​
            //2.添加方法体
            methodBuilder.addStatement(Constant.AROUTER_NAME + ".getInstance().registerActivity(\"" + key + "\"," + className + ")");
​
        }
        //3.生成方法
        MethodSpec methodSpec = methodBuilder.build();
​
        //4.获取接口类
        ClassName iRouter = ClassName.get(Constant.PACKAGE_NAME,Constant.IROUTER);
        //5.创建工具类
        TypeSpec typeSpec = TypeSpec.classBuilder(Constant.CLASS_NAME + "$$" + moudleName)
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(iRouter) //父类
                .addMethod(methodSpec) //添加方法
                .build();
​
        //6.指定目录构建
        JavaFile javaFile = JavaFile.builder(Constant.PACKAGE_NAME,typeSpec).build();
​
        //7.写道文件
        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
​
        }
    }
​
​
}
  • 生成的文件效果如下
public class RouterGroup$$moduletest implements IRouter {
  public void registerActivity() {
 com.cv.router.ARouter.getInstance().registerActivity("/main/login",com.cv.moduletest.LoginActivity.class);
  }
}
  • 在ARouter中实现init方法,触发类文件的方法
   public void init(Context context){
        this.mContext = context;
        //1.得到生成的RouterGroup$$.. 相关文件 找到这些类
        try {
            List<String> clazzes = getClassName();
            if(clazzes.size() > 0){
                for(String className:clazzes){
                    Class<?> activityClazz = Class.forName(className);
                    if(IRouter.class.isAssignableFrom(activityClazz)){
                        //2.是否是IRouter 子类
                        IRouter router = (IRouter) activityClazz.newInstance();
                        router.registerActivity();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private List<String> getClassName() throws IOException {
        List<String> clazzList = new ArrayList<>();
        //加载apk存储路径给DexFile
        DexFile df = new DexFile(mContext.getPackageCodePath());
        Enumeration<String> enumeration = df.entries();
        while (enumeration.hasMoreElements()){
            String className = enumeration.nextElement();
            if(className.contains(Constant.CLASS_NAME)){
                clazzList.add(className);
            }
        }
        return clazzList;
    }

到此就实现了自动化收集类信息。

四、ARouter总结

当你了解了上述方法时,你再去看ARouter的源码,会轻松点,跳转实现原理,都差不多。当然ARouter也支持拦截等功能,想要查看ARouter源码,可以自行在掘金上搜索。这里给出以前看ARouter时做的笔记,只针对客户端使用ARouter时的时序图和文字描述,可能总结写得不全不好,不喜勿喷

  1. 首先在ARouter.getInstance().init()中会调用_ARouter的init()方法,然后回调用after方法,after方法是通过byName形式获取的拦截器Service。
  2. 这里主要是init()方法,里面会构建一个Handler,主要用来启动应用组件跳转和在debug模式下显示提示,然后还有一个线程池,主要是用于执行后续拦截器拦截逻辑,然后这个init中,最重要的应该就是LogisticsCenter.init()方法,在这里面,他会获取arouter.router包名下的所有类文件名,然后加载到Set集合中,然后遍历这些class,Root相关的类反射调用loadInto方法加载到groupIndex集合中,Interceptors相关的类加载到interceptorsIndex中,Providers相关的类加载都providersIndex中。这些类文件都是arouter-compile根据注解生成的,文件名规则是ARouter $ $Root 模 块 名 或 者 是 A R o u t e r 模块名或者是ARouter ARouterProvider $ $模块名,或者是ARouter G r o u p Group Group group名,例如Root相关类的loadInto方法就是把group值和group 相关类匹配放在groupIndex中,然后在需要使用时再去加group相关类的信息。
  3. 我们使用ARouter.getInstance().build().navigation获取Fragment或者跳转时,它先是_ARouter的build方法, 这个方法里,他会bayType形式调用PathReplaceService,对build()方法传入的路径path做修改,然后如果使用RouterPath注解时没有指定group,会获取path中第一个/后面的字符串作为group并返回一个Poscard,内部有一个bundle用于接收传入的参数,然后调用自身的navigation方法,最后还是回调到了 _ARouter的navigation()方法,这个方法内会按需获取加载指定path对应的类信息,首先是从groupIndex里面需要group组名对应的类信息,然后通过反射调用loadInto方法,将该组名下的所有路径对应关系保存到routes Map中,然后去完善传入的Path对应的RouteMeta信息,最后根据元信息的类型,构建对应的信息,并指定provider和fragment的开启绿色通道。然后接下来,就是如果没有开启绿色通道,将利用CountDownlaunch和线程池将所有拦截器按需进行处理,然后通行后,会根据元信息类型,构造相应参数,启动Activity或者反射构建Fragment返回。

五、最长公共子字符串

这周被问到一个问题,android列表上显示的所有数据,如何找出最长公共子标签,我立马想到动态规划,但是总感觉会有更好的实现方式,毕竟LCS问题大多都是给定两个字符串,总不能每两个比较后 (O(n2)),再跟第三个、第四个比较,这样时间复杂度不是很好。最后回过头想想,其实思路应该就是这样的,通过系统API操作,也要这样比较。

/**
 * str1的长度为M,str2的长度为N,生成大小为M*N的矩阵dp
 * dp[i][j] 的含义是str1[0....i] 与 str2[0......j]的公共子序列的长度 
 * 如果dp[i][0] dp[0][i] 为1 后面就都为1
 * @author xxx
 *
 */
public class DemoFive {
    //dp[i][j] 的含义是str1[0....i] 与 str2[0......j]的公共子序列的长度
    public static String findLCS(String A,int n,String B,int m) {
        char[] arrA = A.toCharArray();
        char[] arrB = B.toCharArray();
        // n * m 矩阵 A * B
        int[][] dp = new int[n][m];
​
        int length = 0;
        int start = 0;
        for(int i = 1;i<n;i++) {
            for(int j = 1;j<m;j++) {
                if(arrA[i]== arrB[j] ) {
                    dp[i][j] = dp[i-1][j-1]+1;
                   if(dp[i][j] > length) {
                       length = dp[i][j];
                       start = i - length+1 ; //注意这里 下标是从0开始的
                   }
                }

            }
        }
        String result = A.substring(start,start + length);
        return result;

    }
}

推荐

B站:项目越做越复杂?组件化开发替你解决所有问题

资料:2020年最新Android学习PDF+架构视频+面试文档​

最后在这里我也分享自己收录整理的Android学习笔录,里面对组件化有详细的讲解,希望可以帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,可以分享给身边好友一起学习

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具