App一键切换url环境、一键打包__Android (Java)

文 | Promise Sun


一、背景:

1. 2022上班第一天,整理一下过去的工作,发现这方面的小知识点,去年忘记记录博客了,于是就有了这篇文章。分享给大家,希望对有需要的朋友有帮助。

2. 项目在开发调试过程中,后台的接口域名一般会分生产环境、测试环境、自定义本地环境等等多个url地址环境,供开发人员使用,而且经常会遇到频繁切换url地址的情况,就需要更改接口域名地址,然后AndroidStudio再重新编译运行App,这样就会非常麻烦!
如果我们可以通过在app中直接切换环境,不需要再重新运行打包,是不是就会方便很多呢?

3. 在开发、打包app上线前,有时会需要手动改动很多配置变量,比较麻烦,也很容易遗漏。
如果可以通过配置文件直接设置好,就会避免很多问题。

二、功能和方案:

1.实现主要功能:App一键切换url环境、一键打包。
app应用内一键切换正式、测试环境,无需重新打包。
包括一键打包,无需手动改动过多配置上线变量。

2.实现方案:
主要通过配置本地文件的方式,将所有涉及的相关变量写在配置文件中,然后通过代码实现相关功能。

3.实现功能项目下载地址:
下载本文Demo请点击此处

三、解决方案步骤一:基础配置

1. 新建configs目录

首先在app目录下,新建configs文件夹,在configs下新建auto和release两个文件目录。
auto:此文件夹中存放 开发版本使用的配置文件。
release:此文件夹中存放 线上版本使用的配置文件。
注:这里的文件夹名称是可以自定义的,只要开发的代码中也做相应更改就没问题。)
下图仅供参考:

新建文件夹.jpg

2. 配置文件设置

1)auto目录:

每个文件代表一种url环境的变量配置,环境可以相互切换。**
设置了5个配置文件(包括config.properties、configDev.properties、configPre.properties、configCustom.properties、configProduct.properties),大家可以根据自己的需要进行设置,若不需要这么多开发环境,可以自行删除或者增加相关配置文件。
注:以下文件中的属性配置仅供参考,大家也可以根据实际需要进行设置。)
config.properties文件:

#app 运行环境设置
#环境名
name=develop
#项目环境 url,此处只是示例,需要替换成你自己的域名地址
api.base.url=https://www.jianshu.com/u/d346ccc6f7a4
#是否为线上
isProduct=false
#是否显示Log日志
isShowLog=true
#是否显示JSON格式
isJSON=true

configDev.properties文件:

# dev环境
#环境名
name=develop
#项目环境 url,此处只是示例,需要替换成你自己的
api.base.url=https://blog.csdn.net/sun_promise/dev/
#是否为线上
isProduct=false
#是否显示Log日志
isShowLog=true
#是否显示JSON格式
isJSON=true

configPre.properties文件:

# pre环境
#环境名
name=pre
#项目环境 url,此处只是示例,需要替换成你自己的
api.base.url=https://blog.csdn.net/sun_promise/pre/
#是否为线上
isProduct=false
#是否显示Log日志
isShowLog=true
#是否显示JSON格式
isJSON=true

configCustom.properties文件:

# 自定义测试环境
#环境名
name=custom
#项目环境 url,此处只是示例,需要替换成你自己的域名地址
api.base.url=http://192.168.xx.xx:11008/
#是否为线上
isProduct=false
#是否显示Log日志
isShowLog=true
#是否显示JSON格式
isJSON=true

configProduct.properties文件:

# 正式环境
#环境名
name=product
#项目环境 url,此处只是示例,需要替换成你自己的
api.base.url=https://blog.csdn.net/sun_promise
#是否为线上
isProduct=true
#是否显示Log日志
isShowLog=true
#是否显示JSON格式
isJSON=true

2)release目录:

只有一个文件config.properties,代表线上产品版本,不能切换环境,此文件的配置是给线上真正的用户使用的。
config.properties文件:

# 线上环境
#环境名
name=product
#项目环境 url,此处只是示例,需要替换成你自己的
api.base.url=https://blog.csdn.net/sun_promise

#若以下配置不使用,可以删除不设置
#是否为线上
isProduct=true
#是否显示Log日志
isShowLog=false
#是否显示JSON格式
isJSON=false

3. 配置build.gradle(app下的Module)

apply plugin: 'com.android.application'

android {
    compileSdk 31

    defaultConfig {
        applicationId "com.sun.urlenvconfig"
        minSdk 21
        targetSdk 31
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        // 多渠道打包,AS3.0之后:原因就是使用了productFlavors分包,
        // 解决方法就是在build.gradle中的defaultConfig中添加 一个flavorDimensions "1"就可以了,后面的1一般是跟你的versionCode相同
        // defaultConfig.versionCode
        flavorDimensions "\"${defaultConfig.versionCode}\""

        //记录下利用buildConfigField为项目进行动态配置(对应BuildConfig.class)
        // eg: ----- debug:打印日志,在内网测试.----- release:关闭日志,外网,签名等
        // 已经通过配置文件设置了,此处可以不设置了
//        buildConfigField("boolean", "IS_PRODUCT", "\"${IS_PRODUCT}\"")
//        buildConfigField("boolean", "IS_JSON", "true")
    }

    buildTypes {
        debug {
            minifyEnabled false
            zipAlignEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
//            signingConfig signingConfigs.release
        }
        release {
            minifyEnabled true
            zipAlignEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
//            signingConfig signingConfigs.release
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    sourceSets {
        //开发版本使用的配置文件
        auto {
            assets.srcDirs = ['assets', 'configs/auto']
        }
        // 线上版本使用的配置文件
        product {
            assets.srcDirs = ['assets', 'configs/release']
        }
    }

    //多渠道打包
    productFlavors {
        auto {
            //可以设置app不同环境的名字 货主测试版
            manifestPlaceholders = [app_name: "环境配置测试版"]
        }
        // 线上产品版本
        product {
            manifestPlaceholders = [app_name: "环境配置正式版"]
        }
    }
    // 打包时选择,这些都是可以自己在config.properties文件中自己配置,然后组合打包的。
    //    autoDebug       指定默认测试环境,有 log,可切换
    //    autoRelease     指定默认测试环境,无 log,可切换
    //    productDebug    环境,无 log,不可切换
    //    productRelease  环境,无 log,不可切换

    applicationVariants.all { variant ->
        variant.outputs.all { output ->
            def buildTypeName = variant.buildType.name
            def versionName = defaultConfig.versionName
            def versionCode = defaultConfig.versionCode
            // 多渠道打包的时候,后台不支持中文
            outputFileName = "envconfig-v${versionName}-${versionCode}-${buildTypeName}-${buildTime()}.apk"
        }
    }
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    /*butterknife*/
    api 'com.jakewharton:butterknife:10.2.0'
    annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
    //MMKV 组件
    implementation 'com.tencent:mmkv-static:1.2.7'
}

//设置默认环境:不写参数或者环境名错误,则默认develop环境
setDefaultEnv()

def setDefaultEnv() {
    def envName = envConfig()
    def envConfigDir = "${rootDir}/app/configs/auto/"
    //def envConfigDir = "${rootDir}/app/configs/release/"
    def renameFile = "config.properties"
    println("打包接口环境:${envName}")
    task configCopy(type: Copy) {
        copy {
            delete "${envConfigDir}/${renameFile}"
            from(envConfigDir)
            into(envConfigDir)
            include(envName)
            rename(envName, renameFile)
        }
    }
}
//这里可以更改AndroidStudio的默认运行环境: 更改envName这里对应的值即可。
String envConfig() {
    def envName = "develop"  //默认运行环境设置:pre、custom、product、develop

    if (hasProperty("env")) {
        envName = getPropmerty("env")
    }
    println("配置环境为:${envName}")

    def envFileName = 'configDev'

    if (envName == "develop") {
        envFileName = 'configDev'
    } else if (envName == "pre") {
        envFileName = 'configPre'
    } else if (envName == "custom") {
        envFileName = 'configCustom'
    } else if (envName == "product") {
        envFileName = 'configProduct'
    }
    return envFileName + ".properties"
}

static def buildTime() {
    def date = new Date()
    def formattedDate = date.format('yyyyMMdd_HHmmss')
    return formattedDate
}

4. 配置渠道包的app名

更改清单文件的设置android:label="${app_name}",

<application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="${app_name}"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.UrlEnvironmentConfig">

四、解决方案步骤二:开发代码完善功能

1. 涉及功能代码目录,仅供参考。

功能代码目录.jpg

2. 初始化环境配置

MyApplication类

/**
 * @Author Promise Sun
 */
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //SP框架要在PropertyUtils工具类之前进行初始化,如果你的项目中已有其他的SP工具类,可以直接使用
        MMKV.initialize(this);
        SpUtil.getInstance();

        //Url相关
        PropertyUtils.init(this);

        //设置打印开关
//        LogUtil.setIsLog(PropertyUtils.isShowLog());
    }
}

3. 加载配置文件工具类 PropertyUtils

/**
 * @Author Promise Sun
 */
public class PropertyUtils {

    private static Properties mProps = new Properties();
    private static boolean mHasLoadProps = false;
    private static final Object mLock = new Object();
    private static final String TAG = "PropertyUtils";

    public PropertyUtils() { }

    /**
     * 在AppApplication中初始化
     */
    public static void init(Context context) {
        if (!mHasLoadProps) {
            synchronized (mLock) {
                if (!mHasLoadProps) {
                    try {
                        //获取环境类型
                        ConfigManager.EnvironmentType environmentType = ConfigManager.getDefault().getAppEnv();
                        //Log.e("xyh", "init: " + environmentType.configType + ".properties");
                        InputStream is = context.getAssets().open(environmentType.configType + ".properties");
                        mProps.load(is);
                        mHasLoadProps = true;
                        Log.e(TAG, "load config.properties successfully!");
                    } catch (IOException var4) {
                        Log.e(TAG, "load config.properties error!", var4);
                    }

                }
            }
        }
    }

    public static String getApiBaseUrl() {
        if (mProps == null) {
            throw new IllegalArgumentException("must call #UtilsManager.init(context) in application");
        } else {
            return mProps.getProperty(PropertyKey.BASE_URL, "");
        }
    }

    public static boolean isProduct() {
        return mProps.getProperty(PropertyKey.IS_PRODUCT, "false").equals("true");
    }

    public static boolean isJSON() {
        return mProps.getProperty(PropertyKey.IS_JSON, "false").equals("true");
    }

    public static boolean isShowLog() {
        return mProps.getProperty(PropertyKey.IS_SHOW_LOG, "false").equals("true");
    }

    public static String getEnvironmentName() {
        return mProps.getProperty(PropertyKey.NAME_ENV, "");
    }

    public static ConfigManager.EnvironmentType environmentMap() {
        String envName = getEnvironmentName();
        switch (envName) {
            case "config":
                return ConfigManager.EnvironmentType.DEV;
            case "pre":
                return ConfigManager.EnvironmentType.PRE;
            case "custom":
                return ConfigManager.EnvironmentType.CUSTOM;
            case "product":
                return ConfigManager.EnvironmentType.PRODUCT;
            default:
                return ConfigManager.EnvironmentType.DEFAULT;
        }
    }
}

4. 设置环境配置管理类ConfigManager

/**
 * @Author Promise Sun
 * 环境配置管理类
 */
public class ConfigManager {

    //当前环境
    private EnvironmentType mCurrentEnvType;

    private static final String APP_ENV = "appEnv";

    private ConfigManager() {
    }

    public static ConfigManager getDefault() {
        return HOLDER.INSTANCE;
    }

    private static class HOLDER {
        static ConfigManager INSTANCE = new ConfigManager();
    }

    /***
     * 保存环境:指在切换环境时调用一次
     */
    public void saveAppEnv(EnvironmentType type) {
        SpUtil.setString(APP_ENV, type.configType);
    }


    /***
     * 获取环境类型
     */
    public EnvironmentType getAppEnv() {

        if (mCurrentEnvType == null) {
           // Log.e("sun:", "FLAVOR: " + BuildConfig.FLAVOR);
            String env;
            if (GlobalConstant.AUTO.equals(BuildConfig.FLAVOR)) {
                env = SpUtil.getString(APP_ENV, EnvironmentType.DEFAULT.configType);
                if (TextUtils.isEmpty(env)) {
                    env = EnvironmentType.DEFAULT.configType;
                }
            } else {
                env = EnvironmentType.DEFAULT.configType;
            }
            mCurrentEnvType = EnvironmentType.map(env);
        }
        return mCurrentEnvType;
    }


    //环境类型
    public enum EnvironmentType {

        // 默认环境dev   config:环境配置文件名
        DEFAULT("config"),

        // develop环境
        DEV("configDev"),

        // 自定义测试环境
        CUSTOM("configCustom"),

        // 预发布环境
        PRE("configPre"),

        // 线上环境
        PRODUCT("configProduct");

        String configType;

        EnvironmentType(String configType) {
            this.configType = configType;
        }

        public static EnvironmentType map(String configType) {
            if (TextUtils.equals(EnvironmentType.DEV.configType, configType)) {
                return EnvironmentType.DEV;
            } else if (TextUtils.equals(EnvironmentType.PRE.configType, configType)) {
                return EnvironmentType.PRE;
            } else if (TextUtils.equals(EnvironmentType.CUSTOM.configType, configType)) {
                return EnvironmentType.CUSTOM;
            } else if (TextUtils.equals(EnvironmentType.PRODUCT.configType, configType)) {
                return EnvironmentType.PRODUCT;
            } else {
                return EnvironmentType.DEFAULT;
            }
        }
    }

}

5. PropertyKey :配置文件相关属性设置

@StringDef({PropertyKey.BASE_URL,PropertyKey.IS_PRODUCT
        ,PropertyKey.IS_SHOW_LOG,PropertyKey.IS_JSON
        , PropertyKey.NAME_ENV})
@Retention(RetentionPolicy.SOURCE)
public @interface PropertyKey {
    String NAME_ENV = "name";
    String BASE_URL = "api.base.url";
    String IS_PRODUCT = "isProduct";
    String IS_JSON = "isJSON";
    String IS_SHOW_LOG = "isShowLog";
}

6. 常量类GlobalConstant

/**
 * 常量池
 * @Author Promise Sun
 */
public class GlobalConstant {
    public static final String AUTO="auto";
}

7. SharedPreferences工具类

注:SP工具类可以使用你自己项目中已有的,下面这段可以忽略)

public class SpUtil {


    private static SpUtil mInstance;
    private static MMKV mv;

    private SpUtil() {
        mv = MMKV.defaultMMKV();
    }

    /**
     * 初始化MMKV,只需要初始化一次,建议在Application中初始化
     */
    public static SpUtil getInstance() {
        if (mInstance == null) {
            synchronized (SpUtil.class) {
                if (mInstance == null) {
                    mInstance = new SpUtil();
                }
            }
        }
        return mInstance;
    }

    /**
     * 保存数据的方法,我们需要拿到保存数据的具体类型,然后根据类型调用不同的保存方法
     *
     * @param key
     * @param object
     */
    public static void setFloat(String key, Object object) {
        mv.encode(key, (Float) object);
    }

    public static void setString(String key, Object object) {
        mv.encode(key, (String) object);
    }

    public static void setInt(String key, Object object) {
        mv.encode(key, (Integer) object);
    }

    public static void setDouble(String key, Object object) {
        mv.encode(key, (Double) object);
    }

    public static void setLong(String key, Object object) {
        mv.encode(key, (Long) object);
    }

    public static void setBoolean(String key, Object object) {
        mv.encode(key, (Boolean) object);
    }


    public static void setStringSet(String key, Set<String> sets) {
        mv.encode(key, sets);
    }

    public static void setParcelable(String key, Parcelable obj) {
        mv.encode(key, obj);
    }

    /**
     * 得到保存数据的方法,我们根据默认值得到保存的数据的具体类型,然后调用相对于的方法获取值
     */
    public static Integer getInt(String key) {
        return mv.decodeInt(key, 0);
    }

    public static Double getDouble(String key) {
        return mv.decodeDouble(key, 0.00);
    }

    public static Long getLong(String key) {
        return mv.decodeLong(key, 0L);
    }

    public static Boolean getBoolean(String key) {
        return mv.decodeBool(key, false);
    }

    public static Float getFloat(String key) {
        return mv.decodeFloat(key, 0F);
    }

    public static String getString(String key) {
        return mv.decodeString(key, "");
    }

    public static String getString(String key, String defaultValue) {
        return mv.decodeString(key, defaultValue);
    }

    public static Set<String> getStringSet(String key) {
        return mv.decodeStringSet(key, Collections.<String>emptySet());
    }

    public static Parcelable getParcelable(String key) {
        return mv.decodeParcelable(key, null);
    }

    /**
     * 移除某个key对
     *
     * @param key
     */
    public static void removeByKey(String key) {
        mv.removeValueForKey(key);
    }

    /**
     * 清除所有key
     */
    public static void removeAll() {
        mv.clearAll();
    }

    /**
     * 是否包含某个key
     */

    public static boolean containsKey(String key) {
        return mv.containsKey(key);
    }

}

五、解决方案步骤三:页面切换环境功能实现

1. 功能实现类ChangeUrlEnvActivity

(注:功能已实现,只是UI有点丑,大家可以自行开发设置)

/**
 * @Author Promise Sun
 */
public class ChangeUrlEnvActivity extends AppCompatActivity {

    @BindView(R.id.toolbar_left)
    RelativeLayout ShowBack;
    @BindView(R.id.tvTitle)
    TextView tvTitle;

    @BindView(R.id.tv_env)
    TextView tv_env;
    @BindView(R.id.tv_Env_Show)
    TextView tv_Env_Show;

    @BindView(R.id.group)
    RadioGroup mRadioGroup;

    @BindView(R.id.rb_test)
    RadioButton rb_test;

    @BindView(R.id.et_base_url)
    EditText et_base_url;
    @BindView(R.id.ll_set_env)
    LinearLayout ll_set_env;

    @BindView(R.id.btn_ok)
    Button btn_ok;

    private Unbinder unbinder;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_url_env_change);
        unbinder =ButterKnife.bind(this);
        initView();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbinder.unbind();
    }

    @OnClick({R.id.toolbar_left,R.id.btn_ok})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.toolbar_left:
                finish();
                break;
            default:
                break;
        }
    }

    protected void initView() {
        ShowBack.setVisibility(View.VISIBLE);
        tvTitle.setTextColor(getResources().getColor(R.color.black));
        tvTitle.setText("URL 环境");

        tv_env.setText("当前测试环境:"+ PropertyUtils.getEnvironmentName());
        tv_Env_Show.setText( "url :" + PropertyUtils.getApiBaseUrl());

        ConfigManager.EnvironmentType environmentType = PropertyUtils.environmentMap();

        switch (environmentType) {
            case DEV:
                mRadioGroup.check(R.id.rb_dev);
                break;
            case CUSTOM:
                mRadioGroup.check(R.id.rb_test);
                break;
            case PRE:
                mRadioGroup.check(R.id.rb_pre);
                break;
            case PRODUCT:
                mRadioGroup.check(R.id.rb_product);
                break;
            default:
                mRadioGroup.check(R.id.rb_dev);
                break;
        }

        //点击切换环境
        mRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
            switch (checkedId) {
                case R.id.rb_dev:
                    if (ConfigManager.getDefault().getAppEnv() != ConfigManager.EnvironmentType.DEV) {
                        ConfigManager.getDefault().saveAppEnv(ConfigManager .EnvironmentType.DEV);
                    }
                    setRestart();
                    break;
                case R.id.rb_pre:
                    if (ConfigManager.getDefault().getAppEnv() != ConfigManager.EnvironmentType.PRE) {
                        ConfigManager.getDefault().saveAppEnv(ConfigManager.EnvironmentType.PRE);
                    }
                    setRestart();
                    break;
                case R.id.rb_product:
                    if (ConfigManager.getDefault().getAppEnv() != ConfigManager.EnvironmentType.PRODUCT) {
                        ConfigManager.getDefault().saveAppEnv(ConfigManager.EnvironmentType.PRODUCT);
                    }
                    setRestart();
                    break;
                case R.id.rb_test:
                    if (ConfigManager.getDefault().getAppEnv() != ConfigManager.EnvironmentType.CUSTOM) {
                    ConfigManager.getDefault().saveAppEnv(ConfigManager.EnvironmentType.CUSTOM);
                    }
                    setRestart();
                    break;
            }
        });
    }

    private void setRestart() {
        Toast.makeText(this, "1s后关闭App,重启生效", Toast.LENGTH_SHORT).show();

        //退出app要进行退出登录和去除数据相关

        // system.exit(0)、finish、android.os.Process.killProcess(android.os.Process.myPid())区别:
        //可以杀死当前应用活动的进程,这一操作将会把所有该进程内的资源(包括线程全部清理掉)。
        //当然,由于ActivityManager时刻监听着进程,一旦发现进程被非正常Kill,它将会试图去重启这个进程。这就是为什么,有时候当我们试图这样去结束掉应用时,发现它又自动重新启动的原因。
        //1. System.exit(0) 表示是正常退出。
        //2. System.exit(1) 表示是非正常退出,通常这种退出方式应该放在catch块中。
        //3. Process.killProcess 或 System.exit(0)当前进程确实也被 kill 掉了,但 app 会重新启动。

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                Process.killProcess(Process.myPid());
            }
        }, 1000);
    }
}

2. 布局文件 activity_url_env_change.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

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


    <TextView
        android:id="@+id/tv_env"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:text="当前测试环境:"
        android:textSize="18sp"
        android:textColor="#FFFFFF"
        android:textStyle="bold" />
    <TextView
        android:id="@+id/tv_Env_Show"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="16dp"
        android:paddingRight="16dp"
        android:textSize="18sp" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:text="切换环境,请选择:"
        android:textSize="18sp"
        android:textStyle="bold" />

    <RadioGroup
        android:id="@+id/group"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="25dp"
        android:gravity="center">

        <RadioButton
            android:id="@+id/rb_dev"
            android:layout_width="170dp"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:padding="16dp"
            android:text="dev 环境"
            android:textStyle="bold"/>

        <RadioButton
            android:id="@+id/rb_pre"
            android:layout_width="170dp"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:padding="16dp"
            android:text="pre 环境"
            android:textStyle="bold"/>

        <RadioButton
            android:id="@+id/rb_product"
            android:layout_width="170dp"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:padding="16dp"
            android:text="prod 线上环境"
            android:textStyle="bold"/>

        <RadioButton
            android:id="@+id/rb_test"
            android:layout_width="170dp"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:padding="16dp"
            android:text="自定义测试环境"
            android:textStyle="bold"/>
    </RadioGroup>

    <LinearLayout
        android:id="@+id/ll_set_env"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/white"
        android:visibility="gone"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="16dp"
            android:text="手动设置测试环境:"
            android:textSize="18sp"
            android:textStyle="bold" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/white"
            android:orientation="horizontal">

            <TextView
                android:layout_width="100dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="15dp"
                android:layout_marginRight="10dp"
                android:gravity="center_vertical"
                android:text="URL :"
                android:textSize="16sp" />

            <EditText
                android:id="@+id/et_base_url"
                android:layout_width="match_parent"
                android:layout_height="55dp"
                android:layout_gravity="center_vertical"
                android:layout_marginRight="10dp"
                android:layout_toLeftOf="@+id/iv_user_del"
                android:layout_toRightOf="@+id/iv_icon_username"
                android:background="#ffffffff"
                android:hint="请输入测试环境url"
                android:maxLines="2"
                android:padding="10dp"
                android:singleLine="true"
                android:textColor="#0f30b9"
                android:textColorHint="#999999"
                android:textSize="16sp" />

        </LinearLayout>


        <Button
            android:id="@+id/btn_ok"
            android:layout_width="150dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="100dp"
            android:background="#0f30b9"
            android:text="o k"
            android:textColor="@color/white"
            android:textSize="18sp" />
    </LinearLayout>


</LinearLayout>


下载本文Demo请点击此处


版权声明:本文为博主原创文章,转载请点赞此文并注明出处,谢谢!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容