diff --git a/app/build.gradle b/app/build.gradle index d0e23b3..0b52b29 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,11 @@ -plugins { - id 'com.android.application' -} +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +//plugins { +// id 'com.android.application' +// id 'kotlin-android' +// id 'kotlin-android-extensions' +//} android { applicationVariants.all { variant -> @@ -71,15 +76,15 @@ android { dependencies { - testImplementation 'junit:junit:4.+' +// testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' compileOnly 'com.google.android.things:androidthings:1.0' - // 以下为旧版本VPN客户端的依赖库(去掉kotlin) + // 以下为旧版本VPN客户端的依赖库 implementation fileTree(dir: "libs", include: ["*.jar"]) -// implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.3.1' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.0' implementation "androidx.appcompat:appcompat:$androidx_base_version" implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "androidx.recyclerview:recyclerview:$androidx_base_version" @@ -87,20 +92,28 @@ dependencies { implementation "org.jetbrains.anko:anko-commons:$anko_version" - // 所有的模块都依赖于 quick_android_base (必选) - implementation "com.github.SunnyQjm.quickandroid:quick_android_base:$quick_android_version" - implementation "com.github.SunnyQjm.quickandroid:quick_android_custom_view:$quick_android_version" - implementation "com.github.SunnyQjm.quickandroid:quick_android_easy_bar:$quick_android_version" - // 引入quick_android_mvp模块,来使用网络请求和MVP架构(可选) - implementation "com.github.SunnyQjm.quickandroid:quick_android_mvp:$quick_android_version" - // 引入quick_android_rx_permission模块,来接入链式调用动态请求权限的功能(可选) - implementation "com.github.SunnyQjm.quickandroid:quick_android_rx_permission:$quick_android_version" - // 引入quick_android_qrcode模块,来接入扫码功能(可选) - implementation "com.github.SunnyQjm.quickandroid:quick_android_qrcode:$quick_android_version" - // 引入quick_android_image_picker模块,来接入图片选择,图片裁剪等功能(可选) - implementation "com.github.SunnyQjm.quickandroid:quick_android_image_picker:$quick_android_version" - // 引入文件选择功能(调用系统的文件管理器) - implementation "com.github.SunnyQjm.quickandroid:quick_android_file_picker:$quick_android_version" +// // 所有的模块都依赖于 quick_android_base (必选) +// implementation "com.github.SunnyQjm.quickandroid:quick_android_base:$quick_android_version" +// implementation "com.github.SunnyQjm.quickandroid:quick_android_custom_view:$quick_android_version" +// implementation "com.github.SunnyQjm.quickandroid:quick_android_easy_bar:$quick_android_version" +// // 引入quick_android_mvp模块,来使用网络请求和MVP架构(可选) +// implementation "com.github.SunnyQjm.quickandroid:quick_android_mvp:$quick_android_version" +// // 引入quick_android_rx_permission模块,来接入链式调用动态请求权限的功能(可选) +// implementation "com.github.SunnyQjm.quickandroid:quick_android_rx_permission:$quick_android_version" +// // 引入quick_android_qrcode模块,来接入扫码功能(可选) +//// implementation "com.github.SunnyQjm.quickandroid:quick_android_qrcode:$quick_android_version" +//// // 引入quick_android_image_picker模块,来接入图片选择,图片裁剪等功能(可选) +//// implementation "com.github.SunnyQjm.quickandroid:quick_android_image_picker:$quick_android_version" +//// // 引入文件选择功能(调用系统的文件管理器) +//// implementation "com.github.SunnyQjm.quickandroid:quick_android_file_picker:$quick_android_version" + api project(path: ':quick_android_base') + api project(path: ':quick_android_custom_view') + api project(path: ':quick_android_easy_bar') + api project(path: ':quick_android_mvp') + api project(path: ':quick_android_rx_permission') +// api project(path: ':quick_android_qrcode') +// api project(path: ':quick_android_image_picker') +// api project(path: ':quick_android_file_picker') // loading button implementation 'br.com.simplepass:loading-button-android:2.2.0' @@ -121,13 +134,12 @@ dependencies { implementation "com.scwang.smartrefresh:SmartRefreshLayout:$smart_refresh_layout_version" implementation "com.scwang.smartrefresh:SmartRefreshHeader:$smart_refresh_layout_version" - // brvah implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.49-androidx' implementation 'com.google.android.material:material:1.3.0-alpha01' - testImplementation 'junit:junit:4.13.2' + testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index de96ad8..dd4f699 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,43 @@ - - + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/pkusz/min_vpn_client/APP.java b/app/src/main/java/com/pkusz/min_vpn_client/APP.java new file mode 100644 index 0000000..4221480 --- /dev/null +++ b/app/src/main/java/com/pkusz/min_vpn_client/APP.java @@ -0,0 +1,20 @@ +package com.pkusz.min_vpn_client; +/* + * @Author: Wang Feng + * @Description: + * @Version: 1.0.0 + * @Date: 9:27 2021/6/24 + * @Copyright: MIN-Group;国家重大科技基础设施——未来网络北大实验室;深圳市信息论与未来网络重点实验室 + */ + +import android.app.Application; + +public class APP extends Application { + @Override + public void onCreate() { + super.onCreate(); + + // 网络接口初始化 + // ... + } +} diff --git a/app/src/main/java/com/pkusz/min_vpn_client/activity/base/MINVpnBaseActivity.java b/app/src/main/java/com/pkusz/min_vpn_client/activity/base/MINVpnBaseActivity.java index fea39c7..167e2c0 100644 --- a/app/src/main/java/com/pkusz/min_vpn_client/activity/base/MINVpnBaseActivity.java +++ b/app/src/main/java/com/pkusz/min_vpn_client/activity/base/MINVpnBaseActivity.java @@ -9,10 +9,14 @@ package com.pkusz.min_vpn_client.activity.base; import android.widget.Toast; import org.greenrobot.eventbus.EventBus; -import cn.qjm253.quick_android_mvp.base.activity.BaseQuickAndroidMVPActivity; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; -public abstract class MINVpnBaseActivity> - extends BaseQuickAndroidMVPActivity +import cn.qjm253.quick_android_mvp.base.activity.BaseQuickAndroidMVPActivity; +import cn.qjm253.quick_android_mvp.base.mvp.BasePresenter; + +public abstract class MINVpnBaseActivity

> + extends BaseQuickAndroidMVPActivity

implements MINVpnBaseView{ public void showWarning(String msg){ @@ -37,6 +41,11 @@ public abstract class MINVpnBaseActivity - implements WelcomeActivityContract.View, BaseView { + implements WelcomeActivityContract.View{ @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -30,7 +35,20 @@ public class WelcomeActivity extends MINVpnBaseActivity() { + @Override + public void accept(Throwable throwable) throws Exception { + throwable.printStackTrace(); + } + }); + fingerprintAuthenticate(); + } + public void fingerprintAuthenticate(){ + System.out.println("指纹验证..."); } @NotNull diff --git a/app/src/main/java/com/pkusz/min_vpn_client/extensions/ActivityExtensions.kt b/app/src/main/java/com/pkusz/min_vpn_client/extensions/ActivityExtensions.kt new file mode 100644 index 0000000..8ae2e94 --- /dev/null +++ b/app/src/main/java/com/pkusz/min_vpn_client/extensions/ActivityExtensions.kt @@ -0,0 +1,26 @@ +package cn.qjm253.min_vpn.extensions + +import android.app.Activity +import android.graphics.Rect +import android.view.View +import cn.qjm253.quick_android_base.util.DisplayUtils + +fun Activity.listenKeyboardVisible(onChange: (isOpen: Boolean) -> Unit) { + val contentView = findViewById(android.R.id.content) + val activityRoot = window.decorView + + val rectR = Rect() + var lastOpenState = false + val visibleThreshold = DisplayUtils.dp2px(this, 100) + contentView.viewTreeObserver.addOnGlobalLayoutListener { + activityRoot.getWindowVisibleDisplayFrame(rectR) + val heightDiff = activityRoot.rootView.height - rectR.height() + + // 键盘是否弹出 + val isOpen = heightDiff > visibleThreshold + if (lastOpenState != isOpen) { + onChange(isOpen) + lastOpenState = isOpen + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pkusz/min_vpn_client/extensions/ContextExtensions.kt b/app/src/main/java/com/pkusz/min_vpn_client/extensions/ContextExtensions.kt new file mode 100644 index 0000000..c6093f9 --- /dev/null +++ b/app/src/main/java/com/pkusz/min_vpn_client/extensions/ContextExtensions.kt @@ -0,0 +1,10 @@ +package cn.qjm253.min_vpn.extensions + +import android.content.Context +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat + + +fun Context.easyGetColor(@ColorRes id: Int): Int { + return ContextCompat.getColor(this, id) +} \ No newline at end of file diff --git a/app/src/main/java/com/pkusz/min_vpn_client/extensions/FileUtilsExtensions.kt b/app/src/main/java/com/pkusz/min_vpn_client/extensions/FileUtilsExtensions.kt new file mode 100644 index 0000000..df9b785 --- /dev/null +++ b/app/src/main/java/com/pkusz/min_vpn_client/extensions/FileUtilsExtensions.kt @@ -0,0 +1,37 @@ +package cn.qjm253.min_vpn.extensions + +import android.content.Context +import android.os.Environment +import cn.qjm253.quick_android_base.QuickAndroid +import cn.qjm253.quick_android_base.util.FileUtils; +import java.io.File + +val FileUtils.appDataPath: String + get() { + //val sdDir = File(Environment.getDataDirectory().toString() + "/" + "MIN_VPN" + "/") + if(appContext == null) { + val sdDir = File(Environment.getDataDirectory().toString() + "/" + "MIN_VPN" + "/") + if (!sdDir.exists()) { + sdDir.mkdirs() + } + return sdDir.toString() + } else { + val sdDir = File(appContext!!.getDir("MIN_VPN", Context.MODE_PRIVATE).absolutePath) + if (!sdDir.exists()) { + sdDir.mkdirs() + } + return sdDir.toString() + } + } + +var appContext: Context? = null + get() { + return field + } + set(context: Context?) { + field = context + } + +fun FileUtils.init(context: Context) { + appContext = context +} \ No newline at end of file diff --git a/app/src/main/java/com/pkusz/min_vpn_client/extensions/README.md b/app/src/main/java/com/pkusz/min_vpn_client/extensions/README.md new file mode 100644 index 0000000..31f2264 --- /dev/null +++ b/app/src/main/java/com/pkusz/min_vpn_client/extensions/README.md @@ -0,0 +1,3 @@ +# extensions模块说明 +由于Kotlin的一些特性,例如可以直接扩展一些类的方法,故保留此部分的Kotlin代码。 +(旧版本与quick_android耦合得有些严重) \ No newline at end of file diff --git a/app/src/main/java/com/pkusz/min_vpn_client/Request_API.java b/app/src/main/java/com/pkusz/min_vpn_client/model/Request_API.java similarity index 98% rename from app/src/main/java/com/pkusz/min_vpn_client/Request_API.java rename to app/src/main/java/com/pkusz/min_vpn_client/model/Request_API.java index 5f3ef99..0df942d 100644 --- a/app/src/main/java/com/pkusz/min_vpn_client/Request_API.java +++ b/app/src/main/java/com/pkusz/min_vpn_client/model/Request_API.java @@ -1,4 +1,4 @@ -package com.pkusz.min_vpn_client; +package com.pkusz.min_vpn_client.model; /* * @Author: Wang Feng * @Description: @@ -15,7 +15,6 @@ import java.net.InetSocketAddress; import java.net.Socket; import java.nio.charset.StandardCharsets; -//import cn.qjm253.min_vpn.model.request.BaseRequest; import com.pkusz.min_vpn_client.model.request.BaseRequest; import com.pkusz.min_vpn_client.utils.*; import common.LoggerHelper; diff --git a/build.gradle b/build.gradle index b705c87..ff99cc5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,39 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + // ----------start: 导入quickandroid到本地---------- + ext.kotlin_version = '1.3.72' + ext.okhttp_version = '4.0.0' + ext.persistent_cookie_jar_version = 'v1.0.1' + ext.retrofit_version = '2.6.0' + ext.gson_version = '2.8.6' + ext.glide_version = '4.9.0' + ext.easypermission_version = '3.0.0' + ext.bag_qrcode_zxing_version = '1.3.6' + ext.immersion_bar_version = '3.0.0-beta05' + ext.anko_version = '0.10.8' + ext.parallax_layout_version = '1.0' + ext.logger_vesion = '2.2.0' + ext.brvah_version = '2.9.46' + ext.quick_android_version = '0.1.2' + + ext.rx_android_version = '2.1.1' + ext.rx_java = '2.2.10' + ext.rx_kotlin = '2.2.0' + ext.rx_image_picker_version = '2.5.2' + + ext.appcompact_version = '1.1.0' + ext.constraintlayout_version = '1.1.3' + ext.junit_version = '4.13' + ext.test_runner_version = '1.2.0' + ext.espresso_core_version = '3.2.0' + ext.core_ktx_version = '1.3.0' + + ext.compile_sdk_version = 30 + ext.target_sdk_version = 30 + ext.min_sdk_version = 19 + // -------------end---------------- + + ext.kotlin_version = "1.3.72" ext.quick_android_version = 'v0.1.9' ext.anko_version = '0.10.8' ext.smart_refresh_layout_version = '1.1.0' @@ -11,10 +45,11 @@ buildscript { maven { url 'https://jitpack.io' } google() // jcenter() -// mavenCentral() + mavenCentral() } dependencies { classpath "com.android.tools.build:gradle:4.2.1" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -27,7 +62,7 @@ allprojects { maven { url 'http://maven.aliyun.com/nexus/content/groups/public'} maven { url 'https://jitpack.io' } google() -// mavenCentral() + mavenCentral() // jcenter() // Warning: this repository is going to shut down soon } } diff --git a/gradle.properties b/gradle.properties index 6826e61..6ac4212 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,4 +14,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true + +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true \ No newline at end of file diff --git a/quick_android_base/.gitignore b/quick_android_base/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/quick_android_base/.gitignore @@ -0,0 +1 @@ +/build diff --git a/quick_android_base/README.md b/quick_android_base/README.md new file mode 100644 index 0000000..45c9593 --- /dev/null +++ b/quick_android_base/README.md @@ -0,0 +1,161 @@ +> ## quick_android_base 模块 +> +> 本模块提供了一些常用的工具库,一些常用的Kotlin扩展,以及一些基本功能的封装 + +- ### 初始化 + + ```kotlin + // 在Application中初始化 + QuickAndroid.init() + // 下面这一行输入你应用的英文名,便于为你的应用创建对应名字的应用文件夹等等 + .appName("your_app_name") + // 执行下面一行可以开启应用Activity的侧滑返回支持 + .enableParallaxBack(context) + ``` + + + +- ### 侧滑返回支持说明 + + 1. 所有需要支持侧滑返回的`Activity`都要继承 [BaseQuickAndroidActivity](https://github.com/SunnyQjm/quickandroid/blob/master/quick_android_base/src/main/java/cn/qjm253/quick_android_base/base/activity/BaseQuickAndroidActivity.kt>) ; + + 2. 如果继承自[BaseQuickAndroidActivity](https://github.com/SunnyQjm/quickandroid/blob/master/quick_android_base/src/main/java/cn/qjm253/quick_android_base/base/activity/BaseQuickAndroidActivity.kt>) 的`Activity`需要按业务需求开启和关闭业务需求,可以使用下面两个函数(下面两个函数定义在[BaseQuickAndroidActivity](https://github.com/SunnyQjm/quickandroid/blob/master/quick_android_base/src/main/java/cn/qjm253/quick_android_base/base/activity/BaseQuickAndroidActivity.kt>) 当中,所有集成自该类的Activity均可调用): + + ```kotlin + // 禁止侧滑返回 + disableParallaxBack() + + // 开启侧滑返回 + enableParallaxBack() + ``` + + 3. 本侧滑返回使用的是[ParallaxBackLayout](https://github.com/anzewei/ParallaxBackLayout),如果需要自定义侧滑模式,以及触发侧滑返回的位置等等,可以参考该库提供的配置。 + +- ### Glide支持 + + 1. `quick_android_base`库中已经包含了Glide依赖,引入本依赖库之后不需要再另外引入 + + 2. 当前`quick_android_base`中Glide的版本为`4.9.0` + + 3. 已经定义了[MyAppGlideModule](https://github.com/SunnyQjm/quickandroid/blob/master/quick_android_base/src/main/java/cn/qjm253/quick_android_base/MyAppGlideModule.kt),不需要再定义,可以通过下面的方式使用 + + ```kotlin + GlideApp.with(context) + .load(url) + .into(imageView) + ``` + +- ### 动态权限请求支持 + + 本功能基于google开源的[easypermissions](https://github.com/googlesamples/easypermissions)进行,在所有继承自[BaseQuickAndroidActivity](https://github.com/SunnyQjm/quickandroid/blob/master/quick_android_base/src/main/java/cn/qjm253/quick_android_base/base/activity/BaseQuickAndroidActivity.kt>) 的Activity或者继承自[BaseQuickAndroidFragment](https://github.com/SunnyQjm/quickandroid/blob/master/quick_android_base/src/main/java/cn/qjm253/quick_android_base/base/fragment/BaseQuickAndroidFragment.kt)的Fragment均可使用 + + - 动态请求权限 + + ```kotlin + easyRequestPermissions( + arrayof(Manifest.permissions.CAMERA), + R.string.require_camera_permission_tip, + 0 + ) + ``` + + - 复写如下函数 + + ```kotlin + /** + * 授权成功 + */ + override fun onPermissionsGranted(requestCode: Int, perms: MutableList) { + when(requestCode) { + 0 -> { // 请求摄像头权限 + + } + } + } + + /** + * 授权失败 + */ + override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { + + } + ``` + +- ### `quick_android_base`支持的其它特性 + + - 一些有用的工具类 [utils](https://github.com/SunnyQjm/quickandroid/tree/master/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util) + + - 一些有用的扩展 [extensions](https://github.com/SunnyQjm/quickandroid/tree/master/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions) + + - 便捷的页面跳转扩展(下面所有的扩展函数均可在所有Activity和Fragment中使用,只可在Kotlin项目中使用) + + ```kotlin + // 简单使用,跳转到一个Activity + jumpTo(MainActivity::class.java) + + // 跳转到一个Activity,并处理返回结果(requestCode = 0) + jumpForResult(MainACtivity::class.java, 0) + + // 跳转到一个Activity并传递参数 + jumpTo(MainActivity::class.java, + IntentParam() + .add("param1", 1) + .add("param2", "value2") + .add("param3", arrayof("1", "2", "3")) + ) + + // 跳转到一个Activity,传递参数并处理返回结果 + jumpTo(MainActivity::class.java, + 0, + IntentParam() + .add("param1", 1) + .add("param2", "value2") + .add("param3", arrayof("1", "2", "3")) + ) + ``` + + + +- ### `quick_android_base`引入的所有支持库 + + ```groovy + // gson + api "com.google.code.gson:gson:${gson_version}" + + // Glide + api "com.github.bumptech.glide:glide:${glide_version}" + kapt "com.github.bumptech.glide:compiler:${glide_version}" + + // Easypermission + api "pub.devrel:easypermissions:${easypermission_version}" + + // RxJava + // RxAndroid + api "io.reactivex.rxjava2:rxandroid:${rx_android_version}" + api "io.reactivex.rxjava2:rxjava:${rx_java}" + // RxKotlin + api "io.reactivex.rxjava2:rxkotlin:${rx_kotlin}" + + // BRVAHquick_android_base + api "com.github.CymChad:BaseRecyclerViewAdapterHelper:${brvah_version}" + + // Retrofit + api "com.squareup.retrofit2:retrofit:${retrofit_version}" + api "com.squareup.retrofit2:retrofit:${retrofit_version}" + api "com.squareup.retrofit2:adapter-rxjava2:${retrofit_version}" + api "com.squareup.retrofit2:converter-gson:${retrofit_version}" + + // Okhttp + api "com.squareup.okhttp3:okhttp:${okhttp_version}" + + // Logger + api "com.orhanobut:logger:${logger_vesion}" + + // ParallaxBackLayout + api "com.github.SunnyQjm:ParallaxBackLayout:${parallax_layout_version}" + + // ImmersionBar + api "com.gyf.immersionbar:immersionbar:${immersion_bar_version}" + ``` + + \ No newline at end of file diff --git a/quick_android_base/build.gradle b/quick_android_base/build.gradle new file mode 100644 index 0000000..82ca341 --- /dev/null +++ b/quick_android_base/build.gradle @@ -0,0 +1,84 @@ +apply plugin: 'com.android.library' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion compile_sdk_version + + defaultConfig { + minSdkVersion min_sdk_version + targetSdkVersion target_sdk_version + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "androidx.appcompat:appcompat:${appcompact_version}" + testImplementation "junit:junit:${junit_version}" + implementation "androidx.constraintlayout:constraintlayout:${constraintlayout_version}" + androidTestImplementation "androidx.test:runner:${test_runner_version}" + androidTestImplementation "androidx.test.espresso:espresso-core:${espresso_core_version}" + + // gson + api "com.google.code.gson:gson:${gson_version}" + + // Glide + api "com.github.bumptech.glide:glide:${glide_version}" + kapt "com.github.bumptech.glide:compiler:${glide_version}" + + + // Easypermission + api "pub.devrel:easypermissions:${easypermission_version}" + + // RxJava + // RxAndroid + api "io.reactivex.rxjava2:rxandroid:${rx_android_version}" + api "io.reactivex.rxjava2:rxjava:${rx_java}" + // RxKotlin + api "io.reactivex.rxjava2:rxkotlin:${rx_kotlin}" + + // BRVAH + api "com.github.CymChad:BaseRecyclerViewAdapterHelper:${brvah_version}" + + // Retrofit + api "com.squareup.retrofit2:retrofit:${retrofit_version}" + api "com.squareup.retrofit2:retrofit:${retrofit_version}" + api "com.squareup.retrofit2:adapter-rxjava2:${retrofit_version}" + api "com.squareup.retrofit2:converter-gson:${retrofit_version}" + + // Okhttp + api "com.squareup.okhttp3:okhttp:${okhttp_version}" + + // Logger + api "com.orhanobut:logger:${logger_vesion}" + + // ParallaxBackLayout + api "com.github.SunnyQjm:ParallaxBackLayout:${parallax_layout_version}" + + // ImmersionBar + api "com.gyf.immersionbar:immersionbar:${immersion_bar_version}" + + +} diff --git a/quick_android_base/proguard-rules.pro b/quick_android_base/proguard-rules.pro new file mode 100644 index 0000000..a9f634f --- /dev/null +++ b/quick_android_base/proguard-rules.pro @@ -0,0 +1,177 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# AndroidX +-keep class com.google.android.material.** {*;} +-keep class androidx.** {*;} +-keep public class * extends androidx.** +-keep interface androidx.** {*;} +-dontwarn com.google.android.material.** +-dontnote com.google.android.material.** +-dontwarn androidx.** + +# Glide +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.module.AppGlideModule +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} + +# for DexGuard only +-keepresourcexmlelements manifest/application/meta-data@value=GlideModule + + + + +##---------------Begin: proguard configuration for Gson ---------- +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Application classes that will be serialized/deserialized over Gson +#-keep class com.google.gson.examples.android.model.** { ; } + +# Prevent proguard from stripping interface information from TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +##---------------End: proguard configuration for Gson ---------- + + +##---------------Begin: BRVAH---------------## +-keep class com.chad.library.adapter.** { +*; +} +-keep public class * extends com.chad.library.adapter.base.BaseQuickAdapter +-keep public class * extends com.chad.library.adapter.base.BaseViewHolder +-keepclassmembers class **$** extends com.chad.library.adapter.base.BaseViewHolder { + (...); +} +##---------------End: BRVAH---------------## + + + +##---------------Begin: Okhttp---------------## +-dontwarn com.squareup.okhttp3.** +-keep class com.squareup.okhttp3.** { *;} +-dontwarn okio.** +##---------------End: Okhttp---------------## + + +##---------------Begin: RxJava RxAndroid---------------## +-dontwarn sun.misc.** +-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { + long producerIndex; + long consumerIndex; +} +-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef { + rx.internal.util.atomic.LinkedQueueNode producerNode; +} +-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef { + rx.internal.util.atomic.LinkedQueueNode consumerNode; +} +-dontnote rx.internal.util.PlatformDependent +##---------------End: RxJava RxAndroid---------------## + + +##---------------Begin: Junit---------------## +-keep class org.junit.** { *; } +-dontwarn org.junit.** + +-keep class junit.** { *; } +-dontwarn junit.** +##---------------End: Junit---------------## + + + +##---------------Begin: Retrofit---------------## + +# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and +# EnclosingMethod is required to use InnerClasses. +-keepattributes Signature, InnerClasses, EnclosingMethod + +# Retrofit does reflection on method and parameter annotations. +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations + +# Retain service method parameters when optimizing. +-keepclassmembers,allowshrinking,allowobfuscation interface * { + @retrofit2.http.* ; +} + +# Ignore annotation used for build tooling. +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +# Ignore JSR 305 annotations for embedding nullability information. +-dontwarn javax.annotation.** + +# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. +-dontwarn kotlin.Unit + +# Top-level functions that can only be used by Kotlin. +-dontwarn retrofit2.KotlinExtensions + +# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy +# and replaces all potential values with null. Explicitly keeping the interfaces prevents this. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface <1> + +##---------------End: Retrofit---------------## + + +##---------------Begin: Okhttp---------------## + +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.** + +# A resource is loaded with a relative path so the package of this class must be preserved. +-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* + +# OkHttp platform used only on JVM and when Conscrypt dependency is available. +-dontwarn okhttp3.internal.platform.ConscryptPlatform + +##---------------Begin: End---------------## + +##---------------Begin: ParallaxBackLayout---------------## +-keep public enum com.github.anzewei.parallaxbacklayout.ParallaxBack$** { + **[] $VALUES; + public *; +} +##---------------End: ParallaxBackLayout---------------## + diff --git a/quick_android_base/src/androidTest/java/cn/qjm253/quick_android_base/ExampleInstrumentedTest.java b/quick_android_base/src/androidTest/java/cn/qjm253/quick_android_base/ExampleInstrumentedTest.java new file mode 100644 index 0000000..ac98954 --- /dev/null +++ b/quick_android_base/src/androidTest/java/cn/qjm253/quick_android_base/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package cn.qjm253.quick_android_base; + +import android.content.Context; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("cn.qjm253.quick_android_base.test", appContext.getPackageName()); + } +} diff --git a/quick_android_base/src/main/AndroidManifest.xml b/quick_android_base/src/main/AndroidManifest.xml new file mode 100644 index 0000000..66c424a --- /dev/null +++ b/quick_android_base/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/quick_android_base/src/main/java/NetworkUtil.kt b/quick_android_base/src/main/java/NetworkUtil.kt new file mode 100644 index 0000000..202977a --- /dev/null +++ b/quick_android_base/src/main/java/NetworkUtil.kt @@ -0,0 +1,69 @@ +//package cn.qjm253.quick_android_base.util +// +//import android.content.Context +//import android.net.ConnectivityManager +//import android.net.NetworkInfo +// +// +///** +// * 网络工具类 +// * Created by sunny on 18-2-23. +// */ +// +//class NetworkUtil(context: Context){ +// +// val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager +// +// /** +// * 检测网络是否连接 +// * @return +// */ +// private fun checkNetworkState(): Boolean { +// var flag = false +// //得到网络连接信息 +// //去进行判断网络是否连接 +// if (manager.activeNetworkInfo != null) { +// flag = manager.activeNetworkInfo.isAvailable +// } +// return flag +// } +// +// /** +// * 判断是否连了wifi +// */ +// fun isWifiConnect(): Boolean{ +// val wifiState = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI).state +// return wifiState == NetworkInfo.State.CONNECTED +// } +// +// /** +// * 获取当前网络的类型 +// */ +// fun getConnectedType(context: Context?): Int { +// if (context != null) { +// val mConnectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager +// val mNetworkInfo = mConnectivityManager.activeNetworkInfo +// if (mNetworkInfo != null && mNetworkInfo.isAvailable) { +// return mNetworkInfo.type +// } +// } +// return -1 +// } +// +// +// /** +// * 是否是移动数据连接 +// */ +// fun isMobileConnected(context: Context?): Boolean { +// if (context != null) { +// val mConnectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager +// val mMobileNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE) +// if (mMobileNetworkInfo != null) { +// return mMobileNetworkInfo.isAvailable +// } +// } +// return false +// } +// +// +//} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/MyAppGlideModule.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/MyAppGlideModule.kt new file mode 100644 index 0000000..983b0f5 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/MyAppGlideModule.kt @@ -0,0 +1,7 @@ +package cn.qjm253.quick_android_base + +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule + +@GlideModule +class MyAppGlideModule : AppGlideModule() \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/QuickAndroid.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/QuickAndroid.kt new file mode 100644 index 0000000..860c45e --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/QuickAndroid.kt @@ -0,0 +1,41 @@ +package cn.qjm253.quick_android_base + +import android.app.Application +import android.content.Context +import com.github.anzewei.parallaxbacklayout.ParallaxHelper +import com.orhanobut.logger.AndroidLogAdapter +import com.orhanobut.logger.Logger + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +object QuickAndroid { + var APP_NAME = "QuickAndroid" + var enableParallaxBack = false + + fun appName(appName: String = "quick_android"): QuickAndroid { + APP_NAME = appName + return this + } + + fun init(): QuickAndroid { + Logger.addLogAdapter(AndroidLogAdapter()) + return this + } + + /** + * 开启侧滑 + * 如需修改侧滑触发的模式和位置,参看BaseQuickAndroidActivity + * @see cn.qjm253.quick_android_mvp.base.activity.BaseQuickAndroidActivity + */ + fun enableParallaxBack(context: Application): QuickAndroid { + /** + * 初始化侧滑操作 + * 需要在Application当中初始化 + */ + context.registerActivityLifecycleCallbacks(ParallaxHelper.getInstance()) + enableParallaxBack = true + return this + } +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/base/activity/BaseQuickAndroidActivity.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/base/activity/BaseQuickAndroidActivity.kt new file mode 100644 index 0000000..5dfa23e --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/base/activity/BaseQuickAndroidActivity.kt @@ -0,0 +1,110 @@ +package cn.qjm253.quick_android_base.base.activity + +import android.net.Uri +import android.os.Bundle +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import cn.qjm253.quick_android_base.QuickAndroid +import cn.qjm253.quick_android_base.base.fragment.BaseQuickAndroidFragment +import com.github.anzewei.parallaxbacklayout.ParallaxBack +import com.github.anzewei.parallaxbacklayout.ParallaxHelper +import com.github.anzewei.parallaxbacklayout.widget.ParallaxBackLayout +import pub.devrel.easypermissions.EasyPermissions + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +@ParallaxBack +abstract class BaseQuickAndroidActivity : AppCompatActivity(), + EasyPermissions.PermissionCallbacks, BaseQuickAndroidFragment.OnFragmentInteractionListener { + + companion object { + const val EP_CAMERA = 1 + } + + /** + * 是否需要支持侧滑返回 + */ + open fun isNeedScrollBack() = true + + + /** + * 侧滑是否响应全屏 + * true => 全屏滑动都会触发侧滑 + * false => 边缘滑动才会触发侧滑 + */ + open fun isParallaxBackLayoutFullScreen() = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (QuickAndroid.enableParallaxBack) { // 手动开启侧滑才执行侧滑相关代码 + ParallaxHelper.getParallaxBackLayout(this, true)?.let { layout -> + // 设置侧滑的响应模式 + if (isParallaxBackLayoutFullScreen()) { + layout.setEdgeMode(ParallaxBackLayout.EDGE_MODE_FULL) + } else { + layout.setEdgeMode(ParallaxBackLayout.EDGE_MODE_DEFAULT) + } + + // 设置是否允许触发侧滑返回 + if (isNeedScrollBack()) { + layout.setEnableGesture(true) + } else { + layout.setEnableGesture(false) + } + } + } + } + + /** + * 禁止侧滑返回 + */ + fun disableParallaxBack() { + if(QuickAndroid.enableParallaxBack) { + ParallaxHelper.getParallaxBackLayout(this) + ?.setEnableGesture(false) + } + } + + /** + * 开启侧滑返回 + */ + fun enableParallaxBack() { + if(QuickAndroid.enableParallaxBack){ + ParallaxHelper.getParallaxBackLayout(this) + ?.setEnableGesture(true) + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) + } + + /** + * 授权成功 + */ + override fun onPermissionsGranted(requestCode: Int, perms: MutableList) { + } + + /** + * 授权失败 + */ + override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { + + } + + + open fun easyRequestPermissions(permissions: Array, @StringRes tip: Int, requestCode: Int) { + if (EasyPermissions.hasPermissions(this, *permissions)) { + onPermissionsGranted(requestCode, permissions.toMutableList()) + } else { + EasyPermissions.requestPermissions(this, getString(tip), requestCode, *permissions) + } + } + + override fun onFragmentInteraction(uri: Uri?) { + } +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/base/fragment/BaseQuickAndroidFragment.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/base/fragment/BaseQuickAndroidFragment.kt new file mode 100644 index 0000000..58534c7 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/base/fragment/BaseQuickAndroidFragment.kt @@ -0,0 +1,191 @@ +package cn.qjm253.quick_android_base.base.fragment + +import android.Manifest +import android.content.Context +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import cn.qjm253.quick_android_base.R +import cn.qjm253.quick_android_base.base.activity.BaseQuickAndroidActivity +import cn.qjm253.quick_android_base.params.IntentParam +import pub.devrel.easypermissions.EasyPermissions + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ + +/** + * 支持在第一次创建视图时初始化,且不会重复添加导致重叠 + */ +abstract class BaseQuickAndroidFragment : Fragment(), EasyPermissions.PermissionCallbacks { + companion object { + private const val STATE_SAVE_IS_HIDDEN = "STATE_SAVE_IS_HIDDEN" + + fun newInstance_(clazz: Class, intentParam: IntentParam?): T { + val args = Bundle() + intentParam?.applyBundle(args) + val fragment = clazz.newInstance() + fragment.arguments = args + return fragment + } + } + + protected var mView: View? = null + protected var mListener: OnFragmentInteractionListener? = null + + // 当前视图是否构建完成 + private var prepare: Boolean = false + // 是否是第一次加载数据 + private var first: Boolean = true + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + //当页面重启时,Fragment会被保存回复,而此时再加载Fragment便会导致重叠 + if (savedInstanceState != null) { + val isHidden = savedInstanceState.getBoolean(STATE_SAVE_IS_HIDDEN) + val ft = fragmentManager?.beginTransaction() + if (isHidden) + ft?.hide(this) + else + ft?.show(this) + ft?.commit() + } + + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(STATE_SAVE_IS_HIDDEN, isHidden) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + if(mView == null) { + mView = inflater.inflate(getRes(), container, false) + } + + // 如果之前被添加过,则先移除 + if(mView?.parent != null) { + (mView?.parent as ViewGroup).removeView(mView) + } + return mView + } + + + + /** + * 当视图构建完成时会调用这个回调 + */ + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if(!prepare) { + initView() + prepare = true + } + } + + + /** + * 下面的函数由系统调用 + * 在Fragment可见时加载数据 + * + * @param isVisibleToUser + */ + override fun setUserVisibleHint(isVisibleToUser: Boolean) { + super.setUserVisibleHint(isVisibleToUser) + if(isVisibleToUser) { + onVisible() + } else { + onInvisible() + } + } + + + + protected open fun onInvisible() {} + + protected open fun onVisible() { + //加载数据 + if (prepare && first) { + first = false + initialLoadData() + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is OnFragmentInteractionListener) { + mListener = context + } else { + throw RuntimeException("$context must implement OnFragmentInteractionListener") + } + } + + /** + * Fragment页面的布局文件 + */ + abstract fun getRes(): Int + + /** + * 视图初始化操作 + */ + abstract fun initView() + + /** + * 页面第一次加载时加载数据 + */ + abstract fun initialLoadData() + + /** + * This interface must be implemented by activities that contain this + * fragment to allow an interaction in this fragment to be communicated + * to the activity and potentially other fragments contained in that + * activity. + * + * + * See the Android Training lesson [Communicating with Other Fragments](http://developer.android.com/training/basics/fragments/communicating.html) for more information. + */ + interface OnFragmentInteractionListener { + // TODO: Update argument type and name + fun onFragmentInteraction(uri: Uri?) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) + } + + /** + * 授权成功 + */ + override fun onPermissionsGranted(requestCode: Int, perms: MutableList) { + } + + /** + * 授权失败 + */ + override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { + + } + + + open fun requestPermissions(permissions: Array, @StringRes tip: Int, requestCode: Int) { + activity?.let { + if(EasyPermissions.hasPermissions(it, *permissions)) { + onPermissionsGranted(requestCode, permissions.toMutableList()) + } else { + EasyPermissions.requestPermissions(this, getString(tip), requestCode, *permissions) + } + } + } + + + open fun requestCamera(@StringRes tip: Int = R.string.camera_permission_require) { + requestPermissions(arrayOf(Manifest.permission.CAMERA), tip, BaseQuickAndroidActivity.EP_CAMERA) + } +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/ActivityExtensions.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/ActivityExtensions.kt new file mode 100644 index 0000000..fdd3254 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/ActivityExtensions.kt @@ -0,0 +1,41 @@ +package cn.qjm253.quick_android_base.extensions + +import android.app.Activity +import android.content.Context +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import androidx.annotation.IdRes +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:52 + */ + +/** + * Activity触发震动的扩展 + * @param duration 震动持续时间 + * @param amplitude 震动的强度,限制在 1~255 之间 + */ +fun Activity.vibrator(duration: Long = 200, amplitude: Int = 100) { + val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createOneShot(duration, amplitude)) + } else { + vibrator.vibrate(duration) + } +} + + +fun FragmentActivity.addFragmentToActivity(@IdRes fragmentLayoutId: Int, fragmentGenerator: () -> T): T { + var fragment = this.supportFragmentManager.findFragmentById(fragmentLayoutId) as? T + if(fragment == null) { + fragment = fragmentGenerator() + val transaction = supportFragmentManager.beginTransaction() + transaction.replace(fragmentLayoutId, fragment) + transaction.commit() + } + return fragment +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/AnyExtensions.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/AnyExtensions.kt new file mode 100644 index 0000000..52617a0 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/AnyExtensions.kt @@ -0,0 +1,12 @@ +package cn.qjm253.quick_android_base.extensions + +import cn.qjm253.quick_android_base.util.GsonUtil + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ + +fun Any.toJson(): String { + return GsonUtil.bean2Json(this) +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/ContextExtension.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/ContextExtension.kt new file mode 100644 index 0000000..791a406 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/ContextExtension.kt @@ -0,0 +1,149 @@ +package cn.qjm253.quick_android_base.extensions + +import android.app.Activity +import android.app.DatePickerDialog +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import androidx.annotation.StringRes +import androidx.annotation.StyleRes +import androidx.core.content.ContextCompat +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import androidx.fragment.app.Fragment +import cn.qjm253.quick_android_base.params.IntentParam +import java.util.* + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ + +fun Context.toast(info: String, duration: Int = Toast.LENGTH_SHORT) { + Toast.makeText(this, info, duration) + .show() +} + +fun Context.toast(@StringRes stringRes: Int, duration: Int = Toast.LENGTH_SHORT) { + Toast.makeText(this, stringRes, duration) + .show() +} + + +fun Context.jumpTo(cls: Class<*>, vararg flags: Int) { + val intent = Intent(this, cls) + flags.forEach { + intent.addFlags(it) + } + startActivity(intent) +} + + +fun Context.jumpTo(cls: Class<*>, intentParam: IntentParam? = null, vararg flags: Int) { + val intent = Intent(this, cls) + flags.forEach { + intent.addFlags(it) + } + intentParam?.applyParam(intent) + startActivity(intent) +} + +fun Activity.jumpForResult(cls: Class<*>, requestCode: Int = 0, intentParam: IntentParam? = null) { + val intent = Intent(this, cls) + intentParam?.applyParam(intent) + startActivityForResult(intent, requestCode) +} + + +fun Fragment.jumpForResult(cls: Class<*>, requestCode: Int, intentParam: IntentParam? = null) { + val intent = Intent(activity, cls) + intentParam?.applyParam(intent) + startActivityForResult(intent, requestCode) +} + +///////////////////////////////////////////////////////// +///////// 下面是软键盘相关的扩展 +//////////////////////////////////////////////////////// + +/** + * 切换状态,打开则关闭,关闭则打开 + */ +fun Context.changeSoftKeyboard() { + val im = this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + + if (!im.isActive) + return + im.toggleSoftInput(0, 0) +} + +/** + * 隐藏软键盘 + */ +fun Context.hideSoftKeyboard(view: View) { + val im = this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + if (!im.isActive) + return + //隐藏软键盘 // + im.hideSoftInputFromWindow(view.windowToken, 0) +} + +fun Context.showSoftKeyboard(view: View) { + val im = this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + if (im.isActive) + return + im.showSoftInputFromInputMethod(view.windowToken, 0) +} + + +fun Context.showSelectDateDialog( + @StyleRes style: Int = android.R.style.Theme_DeviceDefault_Dialog, + listener: (View, Int, Int, Int) -> Unit = + { view, year, month, dayOfMonth -> } +) { + val c = Calendar.getInstance() + DatePickerDialog( + this, style, listener, c.get(Calendar.YEAR), + c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH) + ) + .show() +} + +fun Context.showSelectDateDialog( + @StyleRes style: Int = android.R.style.Theme_DeviceDefault_Dialog, + listener: (Long) -> Unit = + { time -> } +) { + val c = Calendar.getInstance() + DatePickerDialog( + this, style, { view, year, month, dayOfMonth -> + c.timeInMillis = 0 + c.set(Calendar.YEAR, year) + c.set(Calendar.MONTH, month) + c.set(Calendar.DAY_OF_MONTH, dayOfMonth) + listener(c.timeInMillis) + }, c.get(Calendar.YEAR), + c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH) + ) + .show() +} + + +fun Context.checkMyPermission(permission: String, granted: () -> Unit = {}, deny: () -> Unit = {}) { + if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { + granted() + } else { + deny() + } +} + + +/** + * 不够位数的在前面补0,保留num的长度位数字 + * @param code + * @return + */ +private fun autoGenericCode(code: String, num: Int): String { + return String.format("%0" + num + "d", Integer.parseInt(code)) +} + diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/FloatExtensions.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/FloatExtensions.kt new file mode 100644 index 0000000..956c6e8 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/FloatExtensions.kt @@ -0,0 +1,20 @@ +package cn.qjm253.quick_android_base.extensions + +import java.lang.StringBuilder +import java.math.RoundingMode +import java.text.DecimalFormat + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ + +fun Double.toFixed(num: Int): String { + val sb = StringBuilder("0.") + for(i in 1..num){ + sb.append("0") + } + val df = DecimalFormat(sb.toString()) + df.roundingMode = RoundingMode.HALF_UP; + return df.format(this) +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/FragmentManagerExtensions.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/FragmentManagerExtensions.kt new file mode 100644 index 0000000..9e1ceb7 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/FragmentManagerExtensions.kt @@ -0,0 +1,44 @@ +package cn.qjm253.quick_android_base.extensions + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager + +/** + * @author SunnyQjm + * @date 19-7-8 下午3:42 + */ + +fun FragmentManager.getLazySingleton(tag: String, clazz: Class): Lazy { + val fragmentManager = this + return object : Lazy { + private var t: T? = null + + private fun _getFragment(): T { + if(t == null) { + t = getFragment(tag, clazz, fragmentManager) + } + return t!! + } + + override val value: T + get() = _getFragment() + + override fun isInitialized(): Boolean { + return t != null + } + } +} + + +private fun getFragment(tag: String, clazz: Class, fragmentManager: FragmentManager): T { + var fragment: T? = fragmentManager.findFragmentByTag(tag) as? T + if (fragment == null) { + fragment = clazz.newInstance() + fragmentManager + .beginTransaction() + .add(fragment, tag) + .commitNow() + } + return fragment!! +} + diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/ImageViewExtensions.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/ImageViewExtensions.kt new file mode 100644 index 0000000..cce0168 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/ImageViewExtensions.kt @@ -0,0 +1,63 @@ +package cn.qjm253.quick_android_base.extensions + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import androidx.annotation.DrawableRes +import android.widget.ImageView +import cn.qjm253.quick_android_base.GlideApp +import com.bumptech.glide.load.resource.bitmap.VideoDecoder.FRAME_OPTION +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import java.security.MessageDigest + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ + + +fun ImageView.load(url: String) { + GlideApp.with(this) + .load(url) + .into(this) +} + +fun ImageView.loadCircle(url: String) { + GlideApp.with(this) + .load(url) + .circleCrop() + .into(this) +} + +fun ImageView.loadCircle(@DrawableRes res: Int) { + GlideApp.with(this) + .load(res) + .circleCrop() + .into(this) +} + +@SuppressLint("CheckResult") +fun ImageView.loadVideoFrame(url: String, frameTimeMicros: Long) { + val requestOptions = RequestOptions.frameOf(frameTimeMicros) + requestOptions.set(FRAME_OPTION, MediaMetadataRetriever.OPTION_CLOSEST); + requestOptions.transform(object : BitmapTransformation() { + override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap { + return toTransform + } + + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + try { + messageDigest.update((context.packageName + "RotateTransform").toByteArray(charset("utf-8"))) + } catch (e: Exception) { + e.printStackTrace() + } + } + + }) + GlideApp.with(this) + .load(url) + .apply(requestOptions) + .into(this) +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/LoggerExtensions.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/LoggerExtensions.kt new file mode 100644 index 0000000..3934e91 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/LoggerExtensions.kt @@ -0,0 +1,29 @@ +package cn.qjm253.quick_android_base.extensions + +import com.orhanobut.logger.Logger + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ + +fun String.e(vararg args: Any) { + Logger.e(this, args) +} + +fun Throwable.e(vararg args: Any) { + Logger.e(this, this.message ?: "", args) +} + + +fun String.i(vararg args: Any) { + Logger.i(this, args) +} + +fun String.d(vararg args: Any) { + Logger.d(this, args) +} + +fun String.json(vararg args: Any) { + Logger.json(this) +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/LongExtensions.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/LongExtensions.kt new file mode 100644 index 0000000..b13403c --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/LongExtensions.kt @@ -0,0 +1,48 @@ +package cn.qjm253.quick_android_base.extensions + +import java.util.* + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ + + +fun Long.toYMD(): String { + val instance = Calendar.getInstance() + instance.timeInMillis = this + return "${instance.get(Calendar.YEAR)}" + + "-${instance.get(Calendar.MONTH) + 1}" + + "-${instance.get(Calendar.DAY_OF_MONTH)}" +} + +fun Long.toYMD_HM(): String { + val instance = Calendar.getInstance() + instance.timeInMillis = this + return "${instance.get(Calendar.YEAR)}" + + "-${instance.get(Calendar.MONTH) + 1}" + + "-${instance.get(Calendar.DAY_OF_MONTH)}" + + " ${instance.get(Calendar.HOUR_OF_DAY)}" + + ":${instance.get(Calendar.MINUTE)}" +} + +fun Long.toYMD_HMS(): String { + val instance = Calendar.getInstance() + instance.timeInMillis = this + return "${instance.get(Calendar.YEAR)}" + + "-${instance.get(Calendar.MONTH) + 1}" + + "-${instance.get(Calendar.DAY_OF_MONTH)}" + + " ${instance.get(Calendar.HOUR_OF_DAY)}" + + ":${instance.get(Calendar.MINUTE)}" + + ":${instance.get(Calendar.SECOND)}" +} + +fun String.toTimeStamp(): Long { + val arr = this.split('-') + val instance = Calendar.getInstance() + instance.timeInMillis = 0 + instance.set(Calendar.YEAR, arr[0].toInt()) + instance.set(Calendar.MONTH, arr[1].toInt() - 1) + instance.set(Calendar.DAY_OF_MONTH, arr[2].toInt()) + return instance.timeInMillis +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/MathExtension.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/MathExtension.kt new file mode 100644 index 0000000..5981753 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/MathExtension.kt @@ -0,0 +1,13 @@ +package cn.qjm253.quick_android_base.extensions + +/** + * Created by sunny on 17-12-29. + */ + +fun easySin(angle: Float, radius:Int = 1): Double { + return Math.sin(Math.PI * angle / 180) * radius +} + +fun easyCos(angle: Float, radius: Int = 1): Double { + return Math.cos(Math.PI * angle / 180) * radius +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/MotionEventExtension.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/MotionEventExtension.kt new file mode 100644 index 0000000..203e8fe --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/MotionEventExtension.kt @@ -0,0 +1,20 @@ +package cn.qjm253.quick_android_base.extensions + +import android.view.MotionEvent +import android.view.View + +/** + * + * Created by sunny on 18-1-2. + */ + +/** + * 该函数用来判断一个触摸事件是否发生在某个具体View的范围内 + */ +fun MotionEvent.isInView(view: View): Boolean { + val location = IntArray(2) + view.getLocationOnScreen(location) + val x = location[0] + val y = location[1] + return !(this.x < x || this.x > x + view.width || this.y < y || this.y > y + view.height) +} diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/MyDelegates.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/MyDelegates.kt new file mode 100644 index 0000000..2bd83f6 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/MyDelegates.kt @@ -0,0 +1,39 @@ +package cn.qjm253.quick_android_base.extensions + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * Created by sunny on 17-12-28. + */ + +object MyDelegates{ + fun notNullAndOnlyInitFirstTime(): ReadWriteProperty = NotNullAndOnlyInitFirstTime() +} + +/** + * 本代理类代理的属性,不可为null,可延迟初始化,但是只会被初始化一次,第一次初始化之后的set操作都将被忽略 + */ +private class NotNullAndOnlyInitFirstTime : ReadWriteProperty { + private var value: T? = null + + /** + * get代理 + * @param thisRef 被代理的对象 + * @param property 保存了被代理对象的名字等 + */ + override fun getValue(thisRef: Any?, property: KProperty<*>): T = + value ?: throw IllegalStateException("${property.name} is not initialized!") + + /** + * set代理 + * @param thisRef 被代理的对象 + * @param property 保存了被代理对象的名字等 + * @param value 元数据 + */ + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + if(this.value == null) + this.value = value + } + +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/TextViewExtensions.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/TextViewExtensions.kt new file mode 100644 index 0000000..82f29a8 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/TextViewExtensions.kt @@ -0,0 +1,37 @@ +package cn.qjm253.quick_android_base.extensions + +import android.annotation.SuppressLint +import android.os.Build +import android.text.Html +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import cn.qjm253.quick_android_base.util.SpannableUtil + +@SuppressLint("ResourceType") +fun TextView.setStyleText(text: String = this.text.toString(), @StringRes textRes: Int = -1, + isBold: Boolean = false, isItalic: Boolean = false, @ColorInt color: Int = -1, + @ColorRes colorRes: Int = -1) { + val t = if (textRes > 0) + context.getString(textRes) + else + text + var result: CharSequence = t + if (isBold) + result = SpannableUtil.bold(result) + if (isItalic) + result = SpannableUtil.italic(result) + if (color > 0 || colorRes > 0) + result = SpannableUtil.color(if (colorRes > 0) resources.getColor(colorRes) else color, result) + this.text = result +} + + +fun TextView.showHtml(html: String) { + this.text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT) + } else { + Html.fromHtml(html) + } +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/ViewExtension.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/ViewExtension.kt new file mode 100644 index 0000000..ea04d95 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/extensions/ViewExtension.kt @@ -0,0 +1,123 @@ +package cn.qjm253.quick_android_base.extensions + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.view.View + +/** + * Created by sunny on 17-12-29. + */ + +/************************************************************************* + * ObjectAnimator生成 + **************************************************************************/ + +fun View.scaleXAnimation(vararg values: Float): ObjectAnimator { + return ObjectAnimator.ofFloat(this, "scaleX", *values) +} + +fun View.scaleYAnimation(vararg values: Float): ObjectAnimator { + return ObjectAnimator.ofFloat(this, "scaleY", *values) +} + +fun View.rotationAnimation(vararg values: Float): ObjectAnimator { + return ObjectAnimator.ofFloat(this, "rotation", *values) +} + +fun View.transXAnimation(vararg values: Float): ObjectAnimator { + return ObjectAnimator.ofFloat(this, "translationX", *values) +} + +fun View.transYAnimation(vararg values: Float): ObjectAnimator { + return ObjectAnimator.ofFloat(this, "translationY", *values) +} + +fun View.alphaAnimation(vararg values: Float): ObjectAnimator { + return ObjectAnimator.ofFloat(this, "alpha", *values) +} +/************************************************************************* + * 属性动画 + **************************************************************************/ + +/** + * XY轴同比例缩放 + */ +fun View.scaleXY(vararg values: Float, duration: Long = 500): View { + val animatorSet = AnimatorSet() + animatorSet.setDuration(duration) + .play(this.scaleXAnimation(*values)) + .with(this.scaleYAnimation(*values)) + animatorSet.start() + return this +} + + + +/** + * 旋转 + */ +fun View.rotate(vararg values: Float, duration: Long = 500, repeatCount: Int = 0, + repeatMode: Int = ValueAnimator.RESTART): ObjectAnimator { + val animation = this.rotationAnimation(*values) + with(animation + .setDuration(duration)){ + this.repeatCount = repeatCount + this.repeatMode = repeatMode + start() + } + return animation +// return this +} + +/** + * 平移 + */ +fun View.transXY(transX: FloatArray = floatArrayOf(0f, 0f), transY: FloatArray = floatArrayOf(0f, 0f) + , duration: Long = 500): View { + val animatorSet = AnimatorSet() + animatorSet.setDuration(duration) + .play(this.transXAnimation(*transX)) + .with(this.transYAnimation(*transY)) + animatorSet + .start() + return this +} + +fun View.doAnim(floats: FloatArray = floatArrayOf(0f, 0f), callback: (value: Float) -> Unit = {}) { + val objAnim = ObjectAnimator.ofFloat(*floats) + objAnim.addUpdateListener { + val value = it.animatedValue as Float + callback(value) + } + objAnim.start() +} + +/** + * 多个属性动画并行运行 + */ +fun View.animer(vararg animations: ObjectAnimator, duration: Long, listener: Animator.AnimatorListener? = null) { + if (animations.isEmpty()) + return + val animatorSet = AnimatorSet() + val builder = animatorSet.setDuration(duration) + .play(animations[0]) + animations.filterIndexed { index, _ -> index != 0 } + .forEach { builder.with(it) } + if (listener != null) + animatorSet.addListener(listener) + animatorSet.start() + +} + +/** + * 平移旋转渐变组合动画 + */ +fun View.transRotateAlpha(transX: FloatArray = floatArrayOf(0f, 0f), transY: FloatArray = floatArrayOf(0f, 0f), + rotation: FloatArray = floatArrayOf(this.rotation, this.rotation), + alpha: FloatArray = floatArrayOf(this.alpha, this.alpha), duration: Long = 500, + listener: Animator.AnimatorListener? = null) { + this.animer(transXAnimation(*transX), transYAnimation(*transY), rotationAnimation(*rotation), + alphaAnimation(*alpha), duration = duration, listener = listener) +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/model/SystemInfo.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/model/SystemInfo.kt new file mode 100644 index 0000000..bb90251 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/model/SystemInfo.kt @@ -0,0 +1,16 @@ +package cn.qjm253.quick_android_base.model + +/** + * @author SunnyQjm + * @date 19-7-10 上午9:25 + */ + +data class SystemInfo( + val brand: String = "", + val model: String = "", + val hardware: String = "", + val sdkInt: Int = 22, + val release: String = "", + val statusBarHeight: Int = 0, + val systemLanguage: String = "" +) \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/params/IntentParam.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/params/IntentParam.kt new file mode 100644 index 0000000..3329d72 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/params/IntentParam.kt @@ -0,0 +1,76 @@ +package cn.qjm253.quick_android_base.params + +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import java.io.Serializable + +/** + * Created by sunny on 18-1-28. + */ +class IntentParam(val keys: MutableList = mutableListOf(), + val values: MutableList = mutableListOf()) { + + fun add(key: String, value: Any?): IntentParam { + if(value == null) + return this + keys.add(key) + values.add(value) + return this + } + + fun applyParam(intent: Intent){ + for(i in 0 until keys.size){ + when(values[i]){ + is Boolean -> intent.putExtra(keys[i], values[i] as Boolean) + is Byte -> intent.putExtra(keys[i], values[i] as Byte) + is Char -> intent.putExtra(keys[i], values[i] as Char) + is Short -> intent.putExtra(keys[i], values[i] as Short) + is Int -> intent.putExtra(keys[i], values[i] as Int) + is Long -> intent.putExtra(keys[i], values[i] as Long) + is Float -> intent.putExtra(keys[i], values[i] as Float) + is Double -> intent.putExtra(keys[i], values[i] as Double) + is String -> intent.putExtra(keys[i], values[i] as String) + is CharSequence -> intent.putExtra(keys[i], values[i] as CharSequence) + is Parcelable -> intent.putExtra(keys[i], values[i] as Parcelable) + is BooleanArray -> intent.putExtra(keys[i], values[i] as BooleanArray) + is ByteArray -> intent.putExtra(keys[i], values[i] as ByteArray) + is CharArray -> intent.putExtra(keys[i], values[i] as CharArray) + is ShortArray -> intent.putExtra(keys[i], values[i] as ShortArray) + is IntArray -> intent.putExtra(keys[i], values[i] as IntArray) + is LongArray -> intent.putExtra(keys[i], values[i] as LongArray) + is FloatArray -> intent.putExtra(keys[i], values[i] as FloatArray) + is DoubleArray -> intent.putExtra(keys[i], values[i] as DoubleArray) + is Serializable -> intent.putExtra(keys[i], values[i] as Serializable) + is Array<*> -> intent.putExtra(keys[i], values[i] as Array) + } + } + } + + fun applyBundle(bundle: Bundle) { + for(i in 0 until keys.size){ + when(values[i]){ + is Boolean -> bundle.putBoolean(keys[i], values[i] as Boolean) + is Byte -> bundle.putByte(keys[i], values[i] as Byte) + is Char -> bundle.putChar(keys[i], values[i] as Char) + is Short -> bundle.putShort(keys[i], values[i] as Short) + is Int -> bundle.putInt(keys[i], values[i] as Int) + is Long -> bundle.putLong(keys[i], values[i] as Long) + is Float -> bundle.putFloat(keys[i], values[i] as Float) + is Double -> bundle.putDouble(keys[i], values[i] as Double) + is String -> bundle.putString(keys[i], values[i] as String) + is CharSequence -> bundle.putCharSequence(keys[i], values[i] as CharSequence) + is Parcelable -> bundle.putParcelable(keys[i], values[i] as Parcelable) + is BooleanArray -> bundle.putBooleanArray(keys[i], values[i] as BooleanArray) + is ByteArray -> bundle.putByteArray(keys[i], values[i] as ByteArray) + is CharArray -> bundle.putCharArray(keys[i], values[i] as CharArray) + is ShortArray -> bundle.putShortArray(keys[i], values[i] as ShortArray) + is IntArray -> bundle.putIntArray(keys[i], values[i] as IntArray) + is LongArray -> bundle.putLongArray(keys[i], values[i] as LongArray) + is FloatArray -> bundle.putFloatArray(keys[i], values[i] as FloatArray) + is DoubleArray -> bundle.putDoubleArray(keys[i], values[i] as DoubleArray) + is Serializable -> bundle.putSerializable(keys[i], values[i] as Serializable) + } + } + } +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/AccountValidatorUtil.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/AccountValidatorUtil.kt new file mode 100644 index 0000000..a9867c6 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/AccountValidatorUtil.kt @@ -0,0 +1,128 @@ +package cn.qjm253.quick_android_base.util + +import java.util.regex.Pattern + +/** + * 账户相关属性验证工具 + */ +object AccountValidatorUtil { + /** + * 正则表达式:验证用户名 + */ + val REGEX_USERNAME = "^[a-zA-Z]\\w{5,20}$" + + /** + * 正则表达式:验证密码 + */ + val REGEX_PASSWORD = "^[a-zA-Z0-9]{6,20}$" + + /** + * 正则表达式:验证手机号 + */ + val REGEX_MOBILE = "^((16[0-9])|(19[0-9])(17[0-9])|(14[0-9])|(13[0-9])|(15[^4,\\D])|(18[0-9]))\\d{8}$" + + /** + * 正则表达式:验证邮箱 + */ + val REGEX_EMAIL = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$" + + /** + * 正则表达式:验证汉字 + */ + val REGEX_CHINESE = "^[\u4e00-\u9fa5],{0,}$" + + /** + * 正则表达式:验证身份证 + */ + val REGEX_ID_CARD = "(^\\d{18}$)|(^\\d{15}$)" + + /** + * 正则表达式:验证URL + */ + val REGEX_URL = "http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?" + + /** + * 正则表达式:验证IP地址 + */ + val REGEX_IP_ADDR = "(25[0-5]|2[0-4]\\d|[0-1]\\d{2}|[1-9]?\\d)" + + /** + * 校验用户名 + * + * @param username + * @return 校验通过返回true,否则返回false + */ + fun isUsername(username: String): Boolean { + return Pattern.matches(REGEX_USERNAME, username) + } + + /** + * 校验密码 + * + * @param password + * @return 校验通过返回true,否则返回false + */ + fun isPassword(password: String): Boolean { + return Pattern.matches(REGEX_PASSWORD, password) + } + + /** + * 校验手机号 + * + * @param mobile + * @return 校验通过返回true,否则返回false + */ + fun isMobile(mobile: String): Boolean { + return Pattern.matches(REGEX_MOBILE, mobile) + } + + /** + * 校验邮箱 + * + * @param email + * @return 校验通过返回true,否则返回false + */ + fun isEmail(email: String): Boolean { + return Pattern.matches(REGEX_EMAIL, email) + } + + /** + * 校验汉字 + * + * @param chinese + * @return 校验通过返回true,否则返回false + */ + fun isChinese(chinese: String): Boolean { + return Pattern.matches(REGEX_CHINESE, chinese) + } + + /** + * 校验身份证 + * + * @param idCard + * @return 校验通过返回true,否则返回false + */ + fun isIDCard(idCard: String): Boolean { + return Pattern.matches(REGEX_ID_CARD, idCard) + } + + /** + * 校验URL + * + * @param url + * @return 校验通过返回true,否则返回false + */ + fun isUrl(url: String): Boolean { + return Pattern.matches(REGEX_URL, url) + } + + /** + * 校验IP地址 + * + * @param ipAddr + * @return + */ + fun isIPAddr(ipAddr: String): Boolean { + return Pattern.matches(REGEX_IP_ADDR, ipAddr) + } +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/ContentUriUtil.java b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/ContentUriUtil.java new file mode 100644 index 0000000..69f6da2 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/ContentUriUtil.java @@ -0,0 +1,366 @@ +package cn.qjm253.quick_android_base.util; + +import android.annotation.SuppressLint; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; + +import androidx.loader.content.CursorLoader; + +import java.io.File; + +public class ContentUriUtil { + /** + * // * Get a file path from a Uri. This will get the the path for Storage Access + * // * Framework Documents, as well as the _data field for the MediaStore and + * // * other file-based ContentProviders. + * // * + * // * @param context The context. + * // * @param uri The Uri to query. + * // * @author paulburke + * // + */ +// public static String getPath(final Context context, final Uri uri) { +// +// final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; +// +// // DocumentProvider +// if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { +// // ExternalStorageProvider +// if (isExternalStorageDocument(uri)) { +// final String docId = DocumentsContract.getDocumentId(uri); +// final String[] split = docId.split(":"); +// final String type = split[0]; +// +// if ("primary".equalsIgnoreCase(type)) { +// return Environment.getExternalStorageDirectory() + "/" + split[1]; +// } +// +// // TODO handle non-primary volumes +// } +// // DownloadsProvider +// else if (isDownloadsDocument(uri)) { +// final String id = DocumentsContract.getDocumentId(uri); +// if (id.startsWith("raw:")) { +// return uri.getPath(); +// } +// final Uri contentUri = ContentUris.withAppendedId( +// Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); +// return getDataColumn(context, contentUri, null, null); +// } +// // MediaProvider +// else if (isMediaDocument(uri)) { +// final String docId = DocumentsContract.getDocumentId(uri); +// final String[] split = docId.split(":"); +// final String type = split[0]; +// +// Uri contentUri = null; +// if ("image".equals(type)) { +// contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; +// } else if ("video".equals(type)) { +// contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; +// } else if ("audio".equals(type)) { +// contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; +// } +// +// final String selection = "_id=?"; +// final String[] selectionArgs = new String[]{ +// split[1] +// }; +// +// return getDataColumn(context, contentUri, selection, selectionArgs); +// } +// } +// // MediaStore (and general) +// else if ("content".equalsIgnoreCase(uri.getScheme())) { +// +// // Return the remote address +// if (isGooglePhotosUri(uri)) +// return uri.getLastPathSegment(); +// +// return getDataColumn(context, uri, null, null); +// } +// // File +// else if ("file".equalsIgnoreCase(uri.getScheme())) { +// return uri.getPath(); +// } +// +// return null; +// } +// +// /** +// * * +// * Get the value of the data column for this Uri. This is useful for +// * MediaStore Uris, and other file-based ContentProviders. +// * +// * @param context The context. +// * @param uri The Uri to query. +// * @param selection (Optional) Filter used in the query. +// * @param selectionArgs (Optional) Selection arguments used in the query. +// * @return The value of the _data column, which is typically a file path. +// */ +// public static String getDataColumn(Context context, Uri uri, String selection, +// String[] selectionArgs) { +// +// Cursor cursor = null; +// final String column = "_data"; +// final String[] projection = { +// column +// }; +// +// try { +// cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, +// null); +// if (cursor != null && cursor.moveToFirst()) { +// final int index = cursor.getColumnIndexOrThrow(column); +// return cursor.getString(index); +// } +// } finally { +// if (cursor != null) +// cursor.close(); +// } +// return null; +// } +// +// +// /** +// * @param uri The Uri to check. +// * @return Whether the Uri authority is ExternalStorageProvider. +// */ +// public static boolean isExternalStorageDocument(Uri uri) { +// return "com.android.externalstorage.documents".equals(uri.getAuthority()); +// } +// +// /** +// * @param uri The Uri to check. +// * @return Whether the Uri authority is DownloadsProvider. +// */ +// public static boolean isDownloadsDocument(Uri uri) { +// return "com.android.providers.downloads.documents".equals(uri.getAuthority()); +// } +// +// /** +// * @param uri The Uri to check. +// * @return Whether the Uri authority is MediaProvider. +// */ +// public static boolean isMediaDocument(Uri uri) { +// return "com.android.providers.media.documents".equals(uri.getAuthority()); +// } +// +// /** +// * @param uri The Uri to check. +// * @return Whether the Uri authority is Google Photos. +// */ +// public static boolean isGooglePhotosUri(Uri uri) { +// return "com.google.android.apps.photos.content".equals(uri.getAuthority()); +// } + + private static String failReason; + + static String errorReason() { + return failReason; + } + + public static String getPath(final Context context, final Uri uri) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + return getRealPathFromURI_BelowAPI19(context, uri); + } + return getRealPathFromURI_API19(context, uri); + } + + + @SuppressLint("NewApi") + public static String getRealPathFromURI_API19(final Context context, final Uri uri) { + + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + if (split.length > 1) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } else { + return Environment.getExternalStorageDirectory() + "/"; + } + } else { + return "storage" + "/" + docId.replace(":", "/"); + } + + } else if (isRawDownloadsDocument(uri)) { + String fileName = getFilePath(context, uri); + String subFolderName = getSubFolders(uri); + + if (fileName != null) { + return Environment.getExternalStorageDirectory().toString() + "/Download/" + subFolderName + fileName; + } + String id = DocumentsContract.getDocumentId(uri); + + final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + return getDataColumn(context, contentUri, null, null); + } else if (isDownloadsDocument(uri)) { + + System.out.println("downloadsDocument"); + String fileName = getFilePath(context, uri); + + if (fileName != null) { + return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName; + } + String id = DocumentsContract.getDocumentId(uri); + if (id.startsWith("raw:")) { + id = id.replaceFirst("raw:", ""); + File file = new File(id); + if (file.exists()) + return id; + } + if (id.startsWith("raw%3A%2F")) { + id = id.replaceFirst("raw%3A%2F", ""); + File file = new File(id); + if (file.exists()) + return id; + } + final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + return getDataColumn(context, contentUri, null, null); + } else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{ + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } else if ("content".equalsIgnoreCase(uri.getScheme())) { + if (isGooglePhotosUri(uri)) { + return uri.getLastPathSegment(); + } + if (getDataColumn(context, uri, null, null) == null) { + failReason = "dataReturnedNull"; + } + return getDataColumn(context, uri, null, null); + } else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + private static String getSubFolders(Uri uri) { + String replaceChars = String.valueOf(uri).replace("%2F", "/").replace("%20", " ").replace("%3A", ":"); + String[] bits = replaceChars.split("/"); + String sub5 = bits[bits.length - 2]; + String sub4 = bits[bits.length - 3]; + String sub3 = bits[bits.length - 4]; + String sub2 = bits[bits.length - 5]; + String sub1 = bits[bits.length - 6]; + if (sub1.equals("Download")) { + return sub2 + "/" + sub3 + "/" + sub4 + "/" + sub5 + "/"; + } else if (sub2.equals("Download")) { + return sub3 + "/" + sub4 + "/" + sub5 + "/"; + } else if (sub3.equals("Download")) { + return sub4 + "/" + sub5 + "/"; + } else if (sub4.equals("Download")) { + return sub5 + "/"; + } else { + return ""; + } + } + + static String getRealPathFromURI_BelowAPI19(Context context, Uri contentUri) { + String[] proj = {MediaStore.Video.Media.DATA}; + CursorLoader loader = new CursorLoader(context, contentUri, proj, null, null, null); + Cursor cursor = loader.loadInBackground(); + assert cursor != null; + int column_index = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA); + cursor.moveToFirst(); + String result = cursor.getString(column_index); + cursor.close(); + return result; + } + + @SuppressWarnings("TryFinallyCanBeTryWithResources") + private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + Cursor cursor = null; + final String column = "_data"; + final String[] projection = {column}; + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); + if (cursor != null && cursor.moveToFirst()) { + final int index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(index); + } + } catch (Exception e) { + failReason = e.getMessage(); + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + + @SuppressWarnings("TryFinallyCanBeTryWithResources") + private static String getFilePath(Context context, Uri uri) { + Cursor cursor = null; + final String[] projection = {MediaStore.Files.FileColumns.DISPLAY_NAME}; + try { + cursor = context.getContentResolver().query(uri, projection, null, null, + null); + if (cursor != null && cursor.moveToFirst()) { + final int index = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME); + return cursor.getString(index); + } + } catch (Exception e) { + failReason = e.getMessage(); + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + + private static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + + private static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + private static boolean isRawDownloadsDocument(Uri uri) { + String uriToString = String.valueOf(uri); + return uriToString.contains("com.android.providers.downloads.documents/document/raw"); + } + + private static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + + private static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/DisplayUtils.java b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/DisplayUtils.java new file mode 100644 index 0000000..918b1d1 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/DisplayUtils.java @@ -0,0 +1,105 @@ +package cn.qjm253.quick_android_base.util; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Rect; +import android.util.TypedValue; +import android.view.WindowManager; + +/** + * Created by liufengkai on 15/11/8. + */ +public class DisplayUtils { + /** + * px to dip 尺寸不变 + * + * @param context + * @param pxValue + * @return + */ + public static int px2dip(Context context, float pxValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + + /** + * dip to px 尺寸不变 + * + * @param context + * @param dipValue + * @return + */ + public static int dip2px(Context context, float dipValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dipValue * scale + 0.5f); + } + + /** + * dp to px + * + * @param context + * @param dp + * @return + */ + public static int dp2px(Context context, int dp) { + return (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp, + context.getResources().getDisplayMetrics()); + } + + /** + * px to sp 文字不变 + * + * @param context + * @param pxValue + * @return + */ + public static int px2sp(Context context, float pxValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (pxValue / fontScale + 0.5f); + } + + /** + * sp to px 文字不变 + * + * @param context + * @param spValue + * @return + */ + public static int sp2px(Context context, float spValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } + + /** + * sp to px + * + * @param context + * @param px + * @return + */ + public static int sp2px(Context context, int px) { + return (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + px, + context.getResources().getDisplayMetrics()); + } + + public static int[] getScreenParams(Context context) { + + WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + + int width = manager.getDefaultDisplay().getWidth(); + int height = manager.getDefaultDisplay().getHeight(); + + return new int[]{width, height}; + } + + public static int getStatusBarHeight(Activity context) { + Rect frame = new Rect(); + context.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame); + return frame.top; + } +} diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/FileUtils.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/FileUtils.kt new file mode 100644 index 0000000..170123d --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/FileUtils.kt @@ -0,0 +1,118 @@ +package cn.qjm253.quick_android_base.util + +import android.os.Environment +import cn.qjm253.quick_android_base.QuickAndroid +import java.io.File + + +/** + * Created by tanshunwang on 2016/9/21 0021. + */ +object FileUtils { + + /** + * 获得应用的根目录 + * + * @return + */ + //判断sd卡是否存在 + //获取根目录 + val appPath: String + get() { + val sdCardExist = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED + val sdDir = if (sdCardExist) { + File(Environment.getExternalStorageDirectory().path + "/" + QuickAndroid.APP_NAME + "/") + } else { + File(Environment.getDataDirectory().toString() + "/" + QuickAndroid.APP_NAME + "/") + } + if (!sdDir.exists()) { + sdDir.mkdirs() + } + return sdDir.toString() + } + + val cachePath: String + get() { + val targetPath = "$appPath/Cache/" + val file = File(targetPath) + if (!file.exists()) + file.mkdirs() + return targetPath + } + + val recordPath: String + get() { + val targetPath = "$appPath/Record/" + val file = File(targetPath) + if (!file.exists()) + file.mkdirs() + return targetPath + } + + val picturePath: String + get() { + val targetPath = "$appPath/Picture/" + val file = File(targetPath) + if (!file.exists()) + file.mkdirs() + return targetPath + } + + val downloadPath: String + get() { + val targetPath = "$appPath/Download/" + val file = File(targetPath) + if (!file.exists()) + file.mkdirs() + return targetPath + } + + val wifiDirectPath: String + get() { + val path = "$appPath/WifiDirectFile/" + val file = File(path) + if (!file.exists()) { + file.mkdirs() + } + return path + } + + + /** + * 获得SDCard的根目录 + * + * @return + */ + //判断sd卡是否存在 + val sdPath: String? + get() { + val sdCardExist = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED + return if (sdCardExist) { + Environment.getExternalStorageDirectory().path + } else { + null + } + } + + + val path: String + get() { + val path = "$appPath/header/" + val file = File(path) + if (!file.exists()) { + file.mkdirs() + println("54555") + } + return path + } + + fun generateRecordFile(fileName: String): File { + return File(recordPath + File.separator + fileName) + } + + fun generatePictureFile(fileName: String): File { + return File(picturePath + File.separator + fileName) + } + + +} diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/GenerateId.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/GenerateId.kt new file mode 100644 index 0000000..b2ab782 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/GenerateId.kt @@ -0,0 +1,30 @@ +package cn.qjm253.quick_android_base.util + +import java.util.concurrent.atomic.AtomicInteger + +/** + * @author SunnyQjm + * @date 19-7-14 下午8:16 + */ + +object GeneratedId { + private val sNextGeneratedId = AtomicInteger(20000) + + /** + * Generate a value suitable for use in setId + * This value will not collide with ID values generated at build time by aapt for R.id. + * + * @return a generated ID value + */ + fun generateViewId(): Int { + while (true) { + val result = sNextGeneratedId.get() + // aapt-generated IDs have the high byte nonzero; clamp to the range under that. + var newValue = result + 1 + if (newValue > 0x00FFFFFF) newValue = 1 // Roll over to 1, not 0. + if (sNextGeneratedId.compareAndSet(result, newValue)) { + return result + } + } + } +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/GsonUtil.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/GsonUtil.kt new file mode 100644 index 0000000..7ddbdb9 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/GsonUtil.kt @@ -0,0 +1,78 @@ +package cn.qjm253.quick_android_base.util + +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.google.gson.reflect.TypeToken + +import java.util.ArrayList + +/** + * Created by Sunny on 2017/4/19 0019. + */ +object GsonUtil { + val instance: Gson = Gson() + + + fun json2Bean(json: String, beanClass: Class): T? { + var bean: T? = null + try { + bean = instance.fromJson(json, beanClass) + } catch (e: JsonSyntaxException) { + e.printStackTrace() + } + + return bean + } + + fun json2Bean(json: String): T? { + var bean: T? = null + val type = object : TypeToken>() { + + }.type + try { + bean = instance.fromJson(json, type) + } catch (e: JsonSyntaxException) { + e.printStackTrace() + } + + return bean + } + + // public static ArrayList json2TagList(String json) { + // Type type = new TypeToken>(){}.getType(); + // ArrayList bean = new ArrayList<>(); + // try { + // bean = getInstance().fromJson(json, type); + // } catch (JsonSyntaxException e) { + // e.printStackTrace(); + // } + // return bean; + // } + // + // public static ArrayList json2RecordList(String json) { + // Type type = new TypeToken>(){}.getType(); + // ArrayList bean = new ArrayList<>(); + // try { + // bean = getInstance().fromJson(json, type); + // } catch (JsonSyntaxException e) { + // e.printStackTrace(); + // } + // return bean; + // } + // + // public static ArrayList json2ImageList(String json) { + // Type type = new TypeToken>(){}.getType(); + // ArrayList bean = new ArrayList<>(); + // try { + // bean = getInstance().fromJson(json, type); + // } catch (JsonSyntaxException e) { + // e.printStackTrace(); + // } + // return bean; + // } + + + fun bean2Json(`object`: Any): String { + return instance.toJson(`object`) + } +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/RxSchedulersHelper.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/RxSchedulersHelper.kt new file mode 100644 index 0000000..976c8f4 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/RxSchedulersHelper.kt @@ -0,0 +1,41 @@ +package cn.qjm253.quick_android_base.util + +import io.reactivex.ObservableTransformer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +/** + * 处理Rx线程转换 + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +object RxSchedulersHelper { + + fun io_main(): ObservableTransformer { + return ObservableTransformer { upstream -> + upstream + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + } + + fun io_io(): ObservableTransformer{ + return ObservableTransformer { upstream -> + upstream.subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + } + } + fun main_io(): ObservableTransformer { + return ObservableTransformer { upstream -> + upstream.subscribeOn(AndroidSchedulers.mainThread()) + .observeOn(Schedulers.io()) + } + } + + fun comppute_main(): ObservableTransformer { + return ObservableTransformer { upstream -> + upstream.subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + } + } +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/SpUtils.java b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/SpUtils.java new file mode 100644 index 0000000..7272bca --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/SpUtils.java @@ -0,0 +1,303 @@ +package cn.qjm253.quick_android_base.util; + +import android.content.Context; +import android.content.SharedPreferences; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * 简化Sp存储的工具类 + * + * @author liufengkai + */ +public class SpUtils { + + public SpUtils() { + /* cannot be instantiated */ + throw new UnsupportedOperationException("cannot be instantiated"); + } + + /** + * Sp文件名 + */ + public static final String FILE_NAME = "share_data"; + + /** + * 保存数据的方法,添加具体数据类型 + * + * @param context + * @param key + * @param object + */ + public static void put(Context context, String key, Object object) { + + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + + // 判断数据类型 + if (object instanceof String) { + editor.putString(key, (String) object); + } else if (object instanceof Integer) { + editor.putInt(key, (Integer) object); + } else if (object instanceof Boolean) { + editor.putBoolean(key, (Boolean) object); + } else if (object instanceof Float) { + editor.putFloat(key, (Float) object); + } else if (object instanceof Long) { + editor.putLong(key, (Long) object); + } else { + editor.putString(key, object.toString()); + } + + SharedPreferencesCompat.apply(editor); + } + + public static void put(Context context, String key, String value) { + + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.putString(key, value); + SharedPreferencesCompat.apply(editor); + } + + public static void put(Context context, String key, int value) { + + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.putInt(key, value); + SharedPreferencesCompat.apply(editor); + } + + public static void put(Context context, String key, float value) { + + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.putFloat(key, value); + SharedPreferencesCompat.apply(editor); + } + + public static void put(Context context, String key, long value) { + + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.putLong(key, value); + SharedPreferencesCompat.apply(editor); + } + + public static String get(Context context, String key, String defaultValue) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + return sp.getString(key, defaultValue); + } + + public static int get(Context context, String key, int defaultValue) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + return sp.getInt(key, defaultValue); + } + + public static float get(Context context, String key, float defaultValue) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + return sp.getFloat(key, defaultValue); + } + + public static long get(Context context, String key, long defaultValue) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + return sp.getLong(key, defaultValue); + } + + public static boolean get(Context context, String key, boolean judge) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + return sp.getBoolean(key, judge); + } + + /** + * 获取保存数据的方法,添加具体数据类型 + * + * @param context + * @param key + * @param defaultObject + * @return object + */ + public static Object get(Context context, String key, Object defaultObject) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + + if (defaultObject instanceof String) { + return sp.getString(key, (String) defaultObject); + } else if (defaultObject instanceof Integer) { + return sp.getInt(key, (Integer) defaultObject); + } else if (defaultObject instanceof Boolean) { + return sp.getBoolean(key, (Boolean) defaultObject); + } else if (defaultObject instanceof Float) { + return sp.getFloat(key, (Float) defaultObject); + } else if (defaultObject instanceof Long) { + return sp.getLong(key, (Long) defaultObject); + } + + return null; + } + + /** + * 存入哈希表 + * + * @param context + * @param map + * @param key + */ + public static void putMap(Context context, Map map, String key) { + JSONArray jsonArray = new JSONArray(); + Set set = map.entrySet(); + Iterator iterator = set.iterator(); + for (int i = 0; i < map.size(); i++) { + Map.Entry mapEntry = (Map.Entry) iterator.next(); + try { + JSONObject object = new JSONObject(); + object.put("key", mapEntry.getKey()); + object.put("value", mapEntry.getValue()); + jsonArray.put(object); + } catch (JSONException e) { + e.printStackTrace(); + } + } + put(context, key, jsonArray.toString()); + } + + /** + * 读取哈希表 + * + * @param context + * @param key + * @return map + */ + public static Map getMap(Context context, String key) { + Map map = new HashMap(); + try { + JSONArray jsonArray = new JSONArray((String) get(context, key, "")); + for (int i = 0; i < jsonArray.length(); i++) { + try { + JSONObject object = jsonArray.getJSONObject(i); + map.put(object.getString("key"), object.getString("value")); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } catch (JSONException e) { + e.printStackTrace(); + } + return map; + } + + /** + * 移除某个key值已经对应的值 + * + * @param context + * @param key + */ + public static void remove(Context context, String key) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.remove(key); + SharedPreferencesCompat.apply(editor); + } + + /** + * 清除所有数据 + * + * @param context + */ + public static void clear(Context context) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.clear(); + SharedPreferencesCompat.apply(editor); + } + + /** + * 查询某个key是否存在 + * + * @param context + * @param key + * @return + */ + public static boolean contains(Context context, String key) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + return sp.contains(key); + } + + /** + * 返回所有的键值对 + * + * @param context + * @return map + */ + public static Map getAll(Context context) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + return sp.getAll(); + } + + /** + * 解决SharedPreferencesCompat.apply方法的兼容类 + * + * @author liufengkai + */ + private static class SharedPreferencesCompat { + + private static final Method sApplyMethod = findApplyMethod(); + + /** + * 反射查找apply的方法 + * + * @return method + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Method findApplyMethod() { + try { + Class clz = SharedPreferences.Editor.class; + return clz.getMethod("apply"); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 如果找到则使用apply执行,否则使用commit + * + * @param editor + */ + public static void apply(SharedPreferences.Editor editor) { + try { + if (sApplyMethod != null) { + sApplyMethod.invoke(editor); + return; + } + } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + editor.commit(); + } + } + +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/SpannableUtil.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/SpannableUtil.kt new file mode 100644 index 0000000..f50c86c --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/SpannableUtil.kt @@ -0,0 +1,85 @@ +package cn.qjm253.quick_android_base.util + +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan + + +/** + * SpannableText的工具类,可以设置文本的显示样式 + * Created by sunny on 18-1-27. + */ + +object SpannableUtil { + /** + * Returns a CharSequence that concatenates the specified array of CharSequence + * objects and then applies a list of zero or more tags to the entire range. + * + * @param content an array of character sequences to apply a style to + * @param tags the styled span objects to apply to the content + * such as android.text.style.StyleSpan + */ + private fun apply(content: Array, vararg tags: Any): CharSequence { + val text = SpannableStringBuilder() + openTags(text, tags) + for (item in content) { + text.append(item) + } + closeTags(text, tags) + return text + } + + /** + * Iterates over an array of tags and applies them to the beginning of the specified + * Spannable object so that future text appended to the text will have the styling + * applied to it. Do not call this method directly. + */ + private fun openTags(text: Spannable, tags: Array) { + for (tag in tags) { + text.setSpan(tag, 0, 0, Spannable.SPAN_MARK_MARK) + } + } + + /** + * "Closes" the specified tags on a Spannable by updating the spans to be + * endpoint-exclusive so that future text appended to the end will not take + * on the same styling. Do not call this method directly. + */ + private fun closeTags(text: Spannable, tags: Array) { + val len = text.length + for (tag in tags) { + if (len > 0) { + text.setSpan(tag, 0, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } else { + text.removeSpan(tag) + } + } + } + + /** + * Returns a CharSequence that applies boldface to the concatenation + * of the specified CharSequence objects. + */ + fun bold(vararg content: CharSequence): CharSequence { + return apply(content, StyleSpan(Typeface.BOLD)) + } + + /** + * Returns a CharSequence that applies italics to the concatenation + * of the specified CharSequence objects. + */ + fun italic(vararg content: CharSequence): CharSequence { + return apply(content, StyleSpan(Typeface.ITALIC)) + } + + /** + * Returns a CharSequence that applies a foreground color to the + * concatenation of the specified CharSequence objects. + */ + fun color(color: Int, vararg content: CharSequence): CharSequence { + return apply(content, ForegroundColorSpan(color)) + } +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/SystemInfoHelper.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/SystemInfoHelper.kt new file mode 100644 index 0000000..1f6fb81 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/SystemInfoHelper.kt @@ -0,0 +1,88 @@ +package cn.qjm253.quick_android_base.util + +import android.content.Context +import cn.qjm253.quick_android_base.model.SystemInfo +import java.util.* + +/** + * 获取系统信息的工具类 + * + * @author SunnyQjm + * @date 19-7-10 上午9:02 + */ + +object SystemInfoHelper { + + /** + * 获取系统状态栏的高度 + */ + fun getStatusBarHeight(context: Context): Int { + val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android") + return context.resources.getDimensionPixelSize(resourceId) + } + + /** + * 获取手机型号 + */ + fun getSystemModel(): String { + return android.os.Build.MODEL ?: "" + } + + /** + * 获取手机厂商 + */ + fun getDeviceBrand(): String { + return android.os.Build.BRAND ?: "" + } + + /** + * 获取手机的版本号 + */ + fun getSystemVersion(): String { + return android.os.Build.VERSION.RELEASE + } + + /** + * 获取当前手机的语言 + */ + fun getSystemLanguage(): String { + return Locale.getDefault().language ?: "" + } + + /** + * 获取当前系统上的语言列表 + */ + fun getSystemLanguageLists(): Array { + return Locale.getAvailableLocales() ?: arrayOf() + } + + /** + * 获取系统硬件信息 + */ + fun getSystemHardware(): String { + return android.os.Build.HARDWARE ?: "" + } + + /** + * 获取当前系统的SDK版本 + */ + fun getSdkInt(): Int { + return android.os.Build.VERSION.SDK_INT + } + + + /** + * 构建系统信息 + */ + fun buildSystemInfo(context: Context): SystemInfo { + return SystemInfo( + getDeviceBrand(), + getSystemModel(), + getSystemHardware(), + getSdkInt(), + getSystemVersion(), + getStatusBarHeight(context), + getSystemLanguage() + ) + } +} \ No newline at end of file diff --git a/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/notification/MyNotificationUtil.kt b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/notification/MyNotificationUtil.kt new file mode 100644 index 0000000..0617f93 --- /dev/null +++ b/quick_android_base/src/main/java/cn/qjm253/quick_android_base/util/notification/MyNotificationUtil.kt @@ -0,0 +1,162 @@ +package cn.qjm253.quick_android_base.util.notification + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import android.graphics.Color +import android.os.Build +import androidx.annotation.DrawableRes +import androidx.core.app.NotificationCompat +import cn.qjm253.quick_android_base.QuickAndroid +import cn.qjm253.quick_android_base.R +import cn.qjm253.quick_android_base.params.IntentParam + +/** + * 通知显示工具类 + * Created by sunny on 18-1-30. + */ + +class MyNotificationUtil private constructor(val context: Context, channelId: String = QuickAndroid.APP_NAME) { + + private var notification: Notification? = null + private var notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private var builder = NotificationCompat.Builder(context, channelId) + + companion object { + private var default_notifyId = 0 + fun newInstance(context: Context): MyNotificationUtil { + return MyNotificationUtil(context) + } + + @Synchronized + fun generateNotifyId(): Int { + default_notifyId = (default_notifyId + 1) % 100 + return default_notifyId + } + + fun createChannelForOreo(context: Context, channelId: String, channelName: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW) + } else { + TODO("VERSION.SDK_INT < O") + } + channel.enableLights(true) + channel.setShowBadge(true) + channel.lightColor = Color.GREEN + channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + + notificationManager.createNotificationChannel(channel) + } + } + } + + fun notify(notification: Notification, notificationId: Int){ + notificationManager.notify(notificationId, notification) + } + + fun cancel(notificationId: Int){ + notificationManager.cancel(notificationId) + } + + + fun initProgress(progress: Int, progressMax: Int = 100, indeterminate: Boolean = true, + title: String = "", content: String = "", ticker: String = ""): MyNotificationUtil { + initNotification(progress = progress, progressMax = progressMax, indeterminate = indeterminate, + title = title, content = content, ticker = ticker, isProgress = true, isSound = false, + isVibrate = false, autoCancel = false) + return this + } + + + fun initNotification(@DrawableRes smallIcon: Int = R.drawable.logo, + @DrawableRes bigIcon: Int = R.drawable.logo, title: String = "", + content: String = "", ticker: String = "", time: Long = System.currentTimeMillis(), + /** + * 1.将AutoCancel设为true后,当你点击通知栏的notification后,它会自动被取消消失, + * 不设置的话点击消息后也不清除,但可以滑动删除 + * 2.将Ongoing设为true 那么notification将不能滑动删除 + */ + autoCancel: Boolean = true, onGoing: Boolean = false, + /** + * 从Android4.1开始,可以通过以下方法,设置notification的优先级, + * 优先级越高的,通知排的越靠前,优先级低的,不会在手机最顶部的状态栏显示图标 + */ + priority: Int = NotificationCompat.PRIORITY_DEFAULT, + /** + * Notification.DEFAULT_ALL:铃声、闪光、震动均系统默认。 + * Notification.DEFAULT_SOUND:系统默认铃声。 + * Notification.DEFAULT_VIBRATE:系统默认震动。 + * Notification.DEFAULT_LIGHTS:系统默认闪光。 + * notifyBuilder.setDefaults(Notification.DEFAULT_ALL); + */ + isSound: Boolean = true, isVibrate: Boolean = true, isLight: Boolean = true, + /** + * 下面是进度相关设置 + */ + progress: Int = 0, progressMax: Int = 100, indeterminate: Boolean = true, + /** + * 是否显示进度条 + */ + isProgress: Boolean = false, + /** + * 点击跳转相关设置 + * 注意事项: setContentIntent一定要在build之前调用,不然无效 + */ + requestCode: Int = 0, target: Class<*>? = null, + pendingIntentFlag: Int = PendingIntent.FLAG_UPDATE_CURRENT, + pendingIntentParam: IntentParam? = null, + + // 适配Android8.0+ 通知必须配置渠道,要不然无法在通知栏显示 + channelId: String = QuickAndroid.APP_NAME, + channelName: String = QuickAndroid.APP_NAME + ) + : MyNotificationUtil { + // 适配Android8.0+ 通知必须配置渠道,要不然无法在通知栏显示 + createChannelForOreo(context, channelId, channelName) + + builder.setAutoCancel(autoCancel) + builder.setSmallIcon(smallIcon) + val bitmap = BitmapFactory.decodeResource(context.resources, bigIcon) + builder.setLargeIcon(bitmap) + builder.setContentText(content) + builder.setContentTitle(title) + builder.setTicker(ticker) + builder.setWhen(time) + builder.priority = priority + builder.setOngoing(onGoing) + var defaults = 0 + if (isSound) { + defaults = defaults or Notification.DEFAULT_SOUND + } + if (isVibrate) { + defaults = defaults or Notification.DEFAULT_VIBRATE + } + if (isLight) { + defaults = defaults or Notification.DEFAULT_LIGHTS + } + + if (isProgress) + builder.setProgress(progressMax, progress, indeterminate) + if (target != null) { + val intent = Intent(context, target) + pendingIntentParam?.applyParam(intent) + builder.setContentIntent(PendingIntent.getActivity(context, requestCode, + intent, pendingIntentFlag)) + } + builder.setDefaults(defaults) + return this + } + + fun show(notificationId: Int = generateNotifyId()): Int { + notification = builder.build() + notificationManager.notify(notificationId, notification) + return notificationId + } +} diff --git a/quick_android_base/src/main/res/drawable/logo.png b/quick_android_base/src/main/res/drawable/logo.png new file mode 100644 index 0000000..bdc6af1 Binary files /dev/null and b/quick_android_base/src/main/res/drawable/logo.png differ diff --git a/quick_android_base/src/main/res/values/strings.xml b/quick_android_base/src/main/res/values/strings.xml new file mode 100644 index 0000000..5c77f4e --- /dev/null +++ b/quick_android_base/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + 本功能需要请求摄像头权限 + diff --git a/quick_android_base/src/test/java/cn/qjm253/quick_android_base/ExampleUnitTest.java b/quick_android_base/src/test/java/cn/qjm253/quick_android_base/ExampleUnitTest.java new file mode 100644 index 0000000..26d7a37 --- /dev/null +++ b/quick_android_base/src/test/java/cn/qjm253/quick_android_base/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package cn.qjm253.quick_android_base; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/quick_android_custom_view/.gitignore b/quick_android_custom_view/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/quick_android_custom_view/.gitignore @@ -0,0 +1 @@ +/build diff --git a/quick_android_custom_view/build.gradle b/quick_android_custom_view/build.gradle new file mode 100644 index 0000000..136d981 --- /dev/null +++ b/quick_android_custom_view/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'com.android.library' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + + +android { + compileSdkVersion compile_sdk_version + + defaultConfig { + minSdkVersion min_sdk_version + targetSdkVersion target_sdk_version + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation "androidx.appcompat:appcompat:${appcompact_version}" + testImplementation "junit:junit:${junit_version}" + androidTestImplementation "androidx.test:runner:${test_runner_version}" + androidTestImplementation "androidx.test.espresso:espresso-core:${espresso_core_version}" + + implementation project(path: ':quick_android_base') + +} diff --git a/quick_android_custom_view/proguard-rules.pro b/quick_android_custom_view/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/quick_android_custom_view/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/quick_android_custom_view/src/androidTest/java/cn/qjm253/quick_android_custom_view/ExampleInstrumentedTest.java b/quick_android_custom_view/src/androidTest/java/cn/qjm253/quick_android_custom_view/ExampleInstrumentedTest.java new file mode 100644 index 0000000..00362ce --- /dev/null +++ b/quick_android_custom_view/src/androidTest/java/cn/qjm253/quick_android_custom_view/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package cn.qjm253.quick_android_custom_view; + +import android.content.Context; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("cn.qjm253.quick_android_custom_view.test", appContext.getPackageName()); + } +} diff --git a/quick_android_custom_view/src/main/AndroidManifest.xml b/quick_android_custom_view/src/main/AndroidManifest.xml new file mode 100644 index 0000000..521a706 --- /dev/null +++ b/quick_android_custom_view/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/custom_drawable_size_view/DrawableButton.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/custom_drawable_size_view/DrawableButton.kt new file mode 100644 index 0000000..00858e4 --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/custom_drawable_size_view/DrawableButton.kt @@ -0,0 +1,102 @@ +package cn.qjm253.quick_android_custom_view.custom_drawable_size_view + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatButton +import cn.qjm253.quick_android_custom_view.R + +/** + * 这是一个Button的封装组件,支持自定义四个方向的drawable的大小 + * + * 1. 使用 'left_drawable', 'right_drawable', 'top_drawable', 'bottom_drawable' 属性来设置四个方向的drawable资源 + * + * 2. 使用 'drawable_width', 'drawable_height' 统一设置四个drawable的大小 + * + * 3. 使用 'left_drawable_width', 'left_drawable_height', 'right_drawable_width', 'right_drawable_height', + * 'top_drawable_width', 'top_drawable_height', 'bottom_drawable_width', 'bottom_drawable_height' + * 来定制指定方向drawable的大小 + * + * @author SunnyQjm + * @date 19-7-10 下午9:09 + */ + + +open class DrawableButton @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.buttonStyle +) : AppCompatButton(context, attrs, defStyleAttr) { + companion object { + const val DEFAULT_SIZE = 20 + } + + init { + val ta = context.obtainStyledAttributes(attrs, R.styleable.DrawableButton) + + val width = ta.getDimensionPixelOffset( + R.styleable.DrawableButton_drawable_width, + DEFAULT_SIZE + ) + val height = ta.getDimensionPixelOffset( + R.styleable.DrawableButton_drawable_height, + DEFAULT_SIZE + ) + + val sizeWrap = SizeWrap() + + val letfD = ta.getDrawable(R.styleable.DrawableButton_left_drawable) + ?.apply { + val lwidth = ta.getDimensionPixelOffset( + R.styleable.DrawableButton_left_drawable_width, + DEFAULT_SIZE + ) + val lheight = ta.getDimensionPixelOffset( + R.styleable.DrawableButton_left_drawable_height, + DEFAULT_SIZE + ) + sizeWrap.checkSize(width, height, lwidth, lheight) + setBounds(0, 0, sizeWrap.width, sizeWrap.height) + } + val rightD = ta.getDrawable(R.styleable.DrawableButton_right_drawable) + ?.apply { + val lwidth = ta.getDimensionPixelOffset( + R.styleable.DrawableButton_right_drawable_width, + DEFAULT_SIZE + ) + val lheight = ta.getDimensionPixelOffset( + R.styleable.DrawableButton_right_drawable_height, + DEFAULT_SIZE + ) + sizeWrap.checkSize(width, height, lwidth, lheight) + setBounds(0, 0, sizeWrap.width, sizeWrap.height) + } + val topD = ta.getDrawable(R.styleable.DrawableButton_top_drawable) + ?.apply { + val lwidth = ta.getDimensionPixelOffset( + R.styleable.DrawableButton_top_drawable_width, + DEFAULT_SIZE + ) + val lheight = ta.getDimensionPixelOffset( + R.styleable.DrawableButton_top_drawable_height, + DEFAULT_SIZE + ) + sizeWrap.checkSize(width, height, lwidth, lheight) + setBounds(0, 0, sizeWrap.width, sizeWrap.height) + } + val bottomD = ta.getDrawable(R.styleable.DrawableButton_bottom_drawable) + ?.apply { + val lwidth = ta.getDimensionPixelOffset( + R.styleable.DrawableButton_bottom_drawable_width, + DEFAULT_SIZE + ) + val lheight = ta.getDimensionPixelOffset( + R.styleable.DrawableButton_bottom_drawable_height, + DEFAULT_SIZE + ) + sizeWrap.checkSize(width, height, lwidth, lheight) + setBounds(0, 0, sizeWrap.width, sizeWrap.height) + } + + this.setCompoundDrawables(letfD, topD, rightD, bottomD) + + ta.recycle() + } +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/custom_drawable_size_view/DrawableEditText.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/custom_drawable_size_view/DrawableEditText.kt new file mode 100644 index 0000000..49fac98 --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/custom_drawable_size_view/DrawableEditText.kt @@ -0,0 +1,75 @@ +package cn.qjm253.quick_android_custom_view.custom_drawable_size_view + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatEditText +import cn.qjm253.quick_android_custom_view.R + +/** + * 这是一个EditText的封装组件,支持自定义四个方向的drawable的大小 + * + * 1. 使用 'left_drawable', 'right_drawable', 'top_drawable', 'bottom_drawable' 属性来设置四个方向的drawable资源 + * + * 2. 使用 'drawable_width', 'drawable_height' 统一设置四个drawable的大小 + * + * 3. 使用 'left_drawable_width', 'left_drawable_height', 'right_drawable_width', 'right_drawable_height', + * 'top_drawable_width', 'top_drawable_height', 'bottom_drawable_width', 'bottom_drawable_height' + * 来定制指定方向drawable的大小 + * + * @author SunnyQjm + * @date 19-7-10 下午9:04 + */ + +open class DrawableEditText @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle +) : AppCompatEditText(context, attrs, defStyleAttr) { + companion object { + const val DEFAULT_SIZE = 20 + } + + init { + val ta = context.obtainStyledAttributes(attrs, R.styleable.DrawableEditText) + + val width = ta.getDimensionPixelOffset(R.styleable.DrawableEditText_drawable_width, DEFAULT_SIZE) + val height = ta.getDimensionPixelOffset(R.styleable.DrawableEditText_drawable_height, DEFAULT_SIZE) + + val sizeWrap = SizeWrap() + + val letfD = ta.getDrawable(R.styleable.DrawableEditText_left_drawable) + ?.apply { + val lwidth = ta.getDimensionPixelOffset(R.styleable.DrawableEditText_left_drawable_width, DEFAULT_SIZE) + val lheight = + ta.getDimensionPixelOffset(R.styleable.DrawableEditText_left_drawable_height, DEFAULT_SIZE) + sizeWrap.checkSize(width, height, lwidth, lheight) + setBounds(0, 0, sizeWrap.width, sizeWrap.height) + } + val rightD = ta.getDrawable(R.styleable.DrawableEditText_right_drawable) + ?.apply { + val lwidth = ta.getDimensionPixelOffset(R.styleable.DrawableEditText_right_drawable_width, DEFAULT_SIZE) + val lheight = + ta.getDimensionPixelOffset(R.styleable.DrawableEditText_right_drawable_height, DEFAULT_SIZE) + sizeWrap.checkSize(width, height, lwidth, lheight) + setBounds(0, 0, sizeWrap.width, sizeWrap.height) + } + val topD = ta.getDrawable(R.styleable.DrawableEditText_top_drawable) + ?.apply { + val lwidth = ta.getDimensionPixelOffset(R.styleable.DrawableEditText_top_drawable_width, DEFAULT_SIZE) + val lheight = ta.getDimensionPixelOffset(R.styleable.DrawableEditText_top_drawable_height, DEFAULT_SIZE) + sizeWrap.checkSize(width, height, lwidth, lheight) + setBounds(0, 0, sizeWrap.width, sizeWrap.height) + } + val bottomD = ta.getDrawable(R.styleable.DrawableEditText_bottom_drawable) + ?.apply { + val lwidth = + ta.getDimensionPixelOffset(R.styleable.DrawableEditText_bottom_drawable_width, DEFAULT_SIZE) + val lheight = + ta.getDimensionPixelOffset(R.styleable.DrawableEditText_bottom_drawable_height, DEFAULT_SIZE) + sizeWrap.checkSize(width, height, lwidth, lheight) + setBounds(0, 0, sizeWrap.width, sizeWrap.height) + } + + this.setCompoundDrawables(letfD, topD, rightD, bottomD) + + ta.recycle() + } +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/custom_drawable_size_view/DrawableTextView.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/custom_drawable_size_view/DrawableTextView.kt new file mode 100644 index 0000000..a6045d5 --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/custom_drawable_size_view/DrawableTextView.kt @@ -0,0 +1,76 @@ +package cn.qjm253.quick_android_custom_view.custom_drawable_size_view + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import cn.qjm253.quick_android_custom_view.R + +/** + * 这是一个TextView的封装组件,支持自定义四个方向的drawable的大小 + * + * 1. 使用 'left_drawable', 'right_drawable', 'top_drawable', 'bottom_drawable' 属性来设置四个方向的drawable资源 + * + * 2. 使用 'drawable_width', 'drawable_height' 统一设置四个drawable的大小 + * + * 3. 使用 'left_drawable_width', 'left_drawable_height', 'right_drawable_width', 'right_drawable_height', + * 'top_drawable_width', 'top_drawable_height', 'bottom_drawable_width', 'bottom_drawable_height' + * 来定制指定方向drawable的大小 + * + * @author SunnyQjm + * @date 19-7-10 下午8:48 + */ + +open class DrawableTextView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.textViewStyle +) : AppCompatTextView(context, attrs, defStyleAttr) { + + companion object { + const val DRAWABLE_DEFAULT_SIZE = 20 + } + + init { + val ta = context.obtainStyledAttributes(attrs, R.styleable.DrawableTextView) + + val width = ta.getDimensionPixelOffset(R.styleable.DrawableTextView_drawable_width, DRAWABLE_DEFAULT_SIZE) + val height = ta.getDimensionPixelOffset(R.styleable.DrawableTextView_drawable_height, DRAWABLE_DEFAULT_SIZE) + + val sizeWrap = SizeWrap() + + val letfD = ta.getDrawable(R.styleable.DrawableTextView_left_drawable) + ?.apply { + val lwidth = ta.getDimensionPixelOffset(R.styleable.DrawableTextView_left_drawable_width, DRAWABLE_DEFAULT_SIZE) + val lheight = ta.getDimensionPixelOffset(R.styleable.DrawableTextView_left_drawable_height, DRAWABLE_DEFAULT_SIZE) + sizeWrap.checkSize(width, height, lwidth, lheight) + setBounds(0, 0, sizeWrap.width, sizeWrap.height) + } + val rightD = ta.getDrawable(R.styleable.DrawableTextView_right_drawable) + ?.apply { + val lwidth = ta.getDimensionPixelOffset(R.styleable.DrawableTextView_right_drawable_width, DRAWABLE_DEFAULT_SIZE) + val lheight = ta.getDimensionPixelOffset(R.styleable.DrawableTextView_right_drawable_height, DRAWABLE_DEFAULT_SIZE) + sizeWrap.checkSize(width, height, lwidth, lheight) + setBounds(0, 0, sizeWrap.width, sizeWrap.height) + } + val topD = ta.getDrawable(R.styleable.DrawableTextView_top_drawable) + ?.apply { + val lwidth = ta.getDimensionPixelOffset(R.styleable.DrawableTextView_top_drawable_width, DRAWABLE_DEFAULT_SIZE) + val lheight = ta.getDimensionPixelOffset(R.styleable.DrawableTextView_top_drawable_height, DRAWABLE_DEFAULT_SIZE) + sizeWrap.checkSize(width, height, lwidth, lheight) + setBounds(0, 0, sizeWrap.width, sizeWrap.height) + } + val bottomD = ta.getDrawable(R.styleable.DrawableTextView_bottom_drawable) + ?.apply { + val lwidth = ta.getDimensionPixelOffset(R.styleable.DrawableTextView_bottom_drawable_width, DRAWABLE_DEFAULT_SIZE) + val lheight = ta.getDimensionPixelOffset(R.styleable.DrawableTextView_bottom_drawable_height, DRAWABLE_DEFAULT_SIZE) + sizeWrap.checkSize(width, height, lwidth, lheight) + setBounds(0, 0, sizeWrap.width, sizeWrap.height) + } + + this.setCompoundDrawables(letfD, topD, rightD, bottomD) + + ta.recycle() + } + + + +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/custom_drawable_size_view/SizeWrap.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/custom_drawable_size_view/SizeWrap.kt new file mode 100644 index 0000000..2cdbd3e --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/custom_drawable_size_view/SizeWrap.kt @@ -0,0 +1,37 @@ +package cn.qjm253.quick_android_custom_view.custom_drawable_size_view + +/** + * 这是一个处理drawable自定义大小设置的工具类 + * + * 1. 四个方向的drawable都有一个默认大小 DRAWABLE_DEFAULT_SIZE; + * + * 2. 使用 'drawable_width' 和 'drawable_height' 来设置全局大小,则四个方向的drawable均被设置; + * + * 3. 如果使用 'left_drawable_width' 和 'left_drawable_height' 这种单独设置某个方向的drawable会覆盖默认大小和全局大小 + * + * @author SunnyQjm + * @date 19-7-10 下午9:00 + */ +class SizeWrap(var width: Int = -1, var height: Int = -1) { + + /** + * 检查用户是否自定义了drawable的size,按上述规则进行判断 + */ + fun checkSize(globalWidth: Int, globalHeight: Int, localWidth: Int, localHeight: Int) { + width = DrawableTextView.DRAWABLE_DEFAULT_SIZE + height = DrawableTextView.DRAWABLE_DEFAULT_SIZE + + // 对某一个具体方向图标大小的设置会覆盖对全局图标大小的设置 + if (localHeight > 0 && localWidth > 0) { + width = localWidth + height = localHeight + } + + + if (localHeight == DrawableTextView.DRAWABLE_DEFAULT_SIZE && localWidth == DrawableTextView.DRAWABLE_DEFAULT_SIZE + && globalHeight > 0 && globalWidth > 0) { + width = globalWidth + height = globalHeight + } + } +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/ClearAbleEditText.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/ClearAbleEditText.kt new file mode 100644 index 0000000..165d02a --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/ClearAbleEditText.kt @@ -0,0 +1,74 @@ +package cn.qjm253.quick_android_custom_view.other + +import android.content.Context +import android.graphics.drawable.Drawable +import android.text.Editable +import android.text.TextWatcher +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import cn.qjm253.quick_android_custom_view.tin_view.TinEditText + +/** + * 这是一个可以显示清空按钮的EditText组件,right_drawable一般设置成有清空含义的图标 + * + * @author SunnyQjm + * @date 19-7-14 下午8:01 + */ + +class ClearAbleEditText @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle +) : TinEditText(context, attrs, defStyleAttr) { + + + init { + changeDrawables(right = null) + addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (s?.length ?: 0 > 0) { + changeDrawables(right = mDrawables[2]) + } else { + changeDrawables(right = null) + } + } + + }) + } + + override fun onFocusChange(v: View?, hasFocus: Boolean) { + super.onFocusChange(v, hasFocus) + if (!hasFocus || text?.isEmpty() == true) + changeDrawables(right = null) + else + changeDrawables(right = mDrawables[2]) + } + + override fun onTouchEvent(event: MotionEvent?): Boolean { + if (event?.action == MotionEvent.ACTION_UP) { + //getTotalPaddingRight()图标左边缘至控件右边缘的距离 + //getWidth() - getTotalPaddingRight()表示从最左边到图标左边缘的位置 + //getWidth() - getPaddingRight()表示最左边到图标右边缘的位置 + val touchable = event.x > width - totalPaddingRight && event.x < width - paddingRight + + if (touchable) { + this.setText("") + } + } + return super.onTouchEvent(event) + } + + private fun changeDrawables( + left: Drawable? = mDrawables[0], + top: Drawable? = mDrawables[1], + right: Drawable? = mDrawables[2], + bottom: Drawable? = mDrawables[3] + ) { + setCompoundDrawables(left, top, right, bottom) + } +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/LineMenu.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/LineMenu.kt new file mode 100644 index 0000000..20e1ea5 --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/LineMenu.kt @@ -0,0 +1,37 @@ +package cn.qjm253.quick_android_custom_view.other + +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.DrawableRes + + +/** + * @author SunnyQjm + * @date 19-7-14 下午8:13 + */ +interface LineMenu { + fun getLeft_icon(): ImageView? + + fun getRight_icon(): ImageView? + + fun getTv_title(): TextView? + + fun getTv_value(): TextView? + + fun setLeftIcon(@DrawableRes res: Int) + + fun setRightIcon(@DrawableRes res: Int) + + + fun setRightIconVisible(visible: Int) + + fun setLeftIconVisible(visible: Int) + + fun setTitleIconVisible(visible: Int) + + fun setValueIconVisible(visible: Int) + + fun setTitle(title: String) + + fun setValue(value: String) +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/LineMenuItem.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/LineMenuItem.kt new file mode 100644 index 0000000..312a091 --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/LineMenuItem.kt @@ -0,0 +1,271 @@ +package cn.qjm253.quick_android_custom_view.other + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import cn.qjm253.quick_android_base.util.GeneratedId +import cn.qjm253.quick_android_custom_view.R + +/** + * 菜单(可以设置图标) + * + * @author SunnyQjm + * @date 19-7-14 下午8:14 + */ + +class LineMenuItem @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, + defStyleAttr: Int = 0) : RelativeLayout(context, attrs, defStyleAttr), LineMenu { + + private var myContext: Context? = null + private var menuTitle: String? = null + private var menuValue: String? = null + private var menuTitleSize: Float = 0.toFloat() + @ColorInt + private var menuTitleColor: Int = 0 + private var menuValueSize: Float = 0.toFloat() + @ColorInt + private var menuValueColor: Int = 0 + @DrawableRes + private var leftResource: Int = 0 + private var rightResource: Int = 0 + + private var isRightIconVisible: Boolean = false + private var isLeftIconVisible: Boolean = false + + private var leftIconSize: Int = 0 + private var rightIconSize: Int = 0 + + private var leftMargin: Int = 0 + private var rightMargin: Int = 0 + private var leftIconTextSpan: Int = 0 + + private var leftIcon: ImageView? = null + private var rightIcon: ImageView? = null + private var tvTitle: TextView? = null + private var tvValue: TextView? = null + + init { + this.myContext = context + getAttr(context, attrs, defStyleAttr) + initView() + } + + private fun getAttr(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + val ta = context.obtainStyledAttributes(attrs, R.styleable.LineMenuItem) + menuTitle = ta.getString(R.styleable.LineMenuItem_menu_title) + menuValue = ta.getString(R.styleable.LineMenuItem_menu_value) + menuTitleSize = px2sp(context, + ta.getDimensionPixelSize(R.styleable.LineMenuItem_menu_title_size, + sp2px(context, DEFAULT_TEXT_SIZE.toFloat())).toFloat()).toFloat() + menuTitleColor = ta.getColor(R.styleable.LineMenuItem_menu_title_color, + resources.getColor(DEFAULT_TITLE_COLOR)) + menuValueSize = px2sp(context, + ta.getDimension(R.styleable.LineMenuItem_menu_value_size, + sp2px(context, DEFAULT_TEXT_SIZE.toFloat()).toFloat())).toFloat() + menuValueColor = ta.getColor(R.styleable.LineMenuItem_menu_value_color, + resources.getColor(DEFAULT_TITLE_COLOR)) + leftResource = ta.getResourceId(R.styleable.LineMenuItem_left_resource, DEFAULT_LEFT_RESOURCE) + rightResource = ta.getResourceId(R.styleable.LineMenuItem_right_resource, DEFAULT_RIGHT_RESOURCE) + leftIconSize = ta.getDimensionPixelSize(R.styleable.LineMenuItem_left_icon_size, + dip2px(context, DEFAULT_LEFT_ICON_SIZE.toFloat())) + rightIconSize = ta.getDimensionPixelSize(R.styleable.LineMenuItem_right_icon_size, + dip2px(context, DEFAULT_RIGHT_ICON_SIZE.toFloat())) + isRightIconVisible = ta + .getBoolean(R.styleable.LineMenuItem_is_right_icon_visible, true) + isLeftIconVisible = ta + .getBoolean(R.styleable.LineMenuItem_is_left_icon_visible, false) + leftMargin = ta.getDimensionPixelSize(R.styleable.LineMenuItem_left_margin, dip2px(context, DEFAULT_MARGIN)) + rightMargin = ta.getDimensionPixelOffset(R.styleable.LineMenuItem_right_margin, dip2px(context, DEFAULT_MARGIN)) + leftIconTextSpan = ta.getDimensionPixelOffset(R.styleable.LineMenuItem_left_icon_text_span, + dip2px(context, DEFAULT_MARGIN)) + ta.recycle() + } + + private fun initView() { + leftIcon = ImageView(myContext) + leftIcon!!.setImageResource(leftResource) + leftIcon!!.scaleType = ImageView.ScaleType.FIT_CENTER + leftIcon!!.setBackgroundResource(R.color.transparent) + val left_icon_param = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + left_icon_param.addRule(CENTER_VERTICAL) + left_icon_param.width = leftIconSize + left_icon_param.height = leftIconSize + left_icon_param.leftMargin = leftMargin + leftIcon!!.layoutParams = left_icon_param + addView(leftIcon, left_icon_param) + if (!isLeftIconVisible) { + leftIcon!!.visibility = View.GONE + } + val left_icon_id = GeneratedId.generateViewId() + leftIcon!!.id = left_icon_id + + tvTitle = TextView(myContext) + tvTitle!!.text = menuTitle + tvTitle!!.setTextColor(menuTitleColor) + tvTitle!!.textSize = menuTitleSize + tvTitle!!.setBackgroundResource(R.color.transparent) + val title_param = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + title_param.addRule(CENTER_VERTICAL) + title_param.leftMargin = leftIconTextSpan + title_param.addRule(RIGHT_OF, left_icon_id) + tvTitle!!.layoutParams = title_param + addView(tvTitle, title_param) + + + rightIcon = ImageView(myContext) + rightIcon!!.setImageResource(rightResource) + rightIcon!!.scaleType = ImageView.ScaleType.FIT_CENTER + rightIcon!!.setBackgroundResource(R.color.transparent) + val right_icon_param = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + right_icon_param.addRule(CENTER_VERTICAL) + right_icon_param.addRule(ALIGN_PARENT_RIGHT) + right_icon_param.width = rightIconSize + right_icon_param.height = rightIconSize + right_icon_param.rightMargin = rightMargin + rightIcon!!.layoutParams = right_icon_param + addView(rightIcon, right_icon_param) + val right_icon_id = GeneratedId.generateViewId() + rightIcon!!.id = right_icon_id + + tvValue = TextView(myContext) + tvValue!!.text = menuValue + tvValue!!.setTextColor(menuValueColor) + tvValue!!.textSize = menuValueSize + tvValue!!.setBackgroundResource(R.color.transparent) + val tv_value_param = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + tv_value_param.addRule(CENTER_VERTICAL) + if (isRightIconVisible) + tv_value_param.addRule(LEFT_OF, right_icon_id) + else + tv_value_param.addRule(ALIGN_PARENT_RIGHT) + tv_value_param.rightMargin = dip2px(myContext!!, DEFAULT_MARGIN) + tvValue!!.layoutParams = tv_value_param + addView(tvValue, tv_value_param) + + if (isRightIconVisible) + setRightIconVisible(View.VISIBLE) + else + setRightIconVisible(View.INVISIBLE) + } + + override fun getLeft_icon(): ImageView? { + return leftIcon + } + + override fun getRight_icon(): ImageView? { + return rightIcon + } + + override fun getTv_title(): TextView? { + return tvTitle + } + + override fun getTv_value(): TextView? { + return tvValue + } + + fun value(): String { + return getTv_value()?.text?.toString() ?: "" + } + + fun title(): String { + return getTv_title()?.text?.toString() ?: "" + } + + override fun setLeftIcon(@DrawableRes res: Int) { + this.post { + leftResource = res + leftIcon!!.setImageResource(leftResource) + } + } + + override fun setRightIcon(@DrawableRes res: Int) { + this.post { + rightResource = res + rightIcon!!.setImageResource(rightResource) + } + } + + override fun setRightIconVisible(visible: Int) { + if (rightIcon != null) + rightIcon!!.visibility = visible + } + + override fun setLeftIconVisible(visible: Int) { + if (leftIcon != null) + leftIcon!!.visibility = visible + } + + override fun setTitleIconVisible(visible: Int) { + if (tvTitle != null) + tvTitle!!.visibility = visible + } + + override fun setValueIconVisible(visible: Int) { + if (tvValue != null) + tvValue!!.visibility = visible + } + + override fun setTitle(title: String) { + if (tvTitle != null) + tvTitle!!.text = title + } + + override fun setValue(value: String) { + if (tvValue != null) + tvValue!!.text = value + } + + companion object { + + private val DEFAULT_TEXT_SIZE = 14 + private val DEFAULT_MARGIN = 13f + + private val DEFAULT_TITLE_COLOR = R.color.text_gray + private val DEFAULT_LEFT_RESOURCE = R.drawable.menu + private val DEFAULT_RIGHT_RESOURCE = R.drawable.icon_arrow + private val DEFAULT_LEFT_ICON_SIZE = 24 + private val DEFAULT_RIGHT_ICON_SIZE = 24 + } + + + /** + * 根据手机的分辨率从 dp 的单位 转成为 px(像素) + */ + fun dip2px(context: Context, dpValue: Float): Int { + val scale = context.resources.displayMetrics.density + return (dpValue * scale + 0.5f).toInt() + } + + /** + * 将px值转换为sp值,保证文字大小不变 + * + * @param pxValue (DisplayMetrics类中属性scaledDensity) + * @return + */ + fun px2sp(context: Context, pxValue: Float): Int { + val fontScale = context.resources.displayMetrics.scaledDensity + return (pxValue / fontScale + 0.5f).toInt() + } + + /** + * 将sp值转换为px值,保证文字大小不变 + * + * @param spValue (DisplayMetrics类中属性scaledDensity) + * @return + */ + fun sp2px(context: Context, spValue: Float): Int { + val fontScale = context.resources.displayMetrics.scaledDensity + return (spValue * fontScale + 0.5f).toInt() + } +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/LineMenuSwitch.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/LineMenuSwitch.kt new file mode 100644 index 0000000..46d6a85 --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/LineMenuSwitch.kt @@ -0,0 +1,154 @@ +package cn.qjm253.quick_android_custom_view.other + +import android.widget.CompoundButton +import android.content.Context +import android.widget.RelativeLayout +import android.os.Build +import android.util.AttributeSet +import android.view.ViewGroup +import android.widget.Switch +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import cn.qjm253.quick_android_custom_view.R + + +/** + * @author SunnyQjm + * @date 19-7-14 下午8:30 + */ +class LineMenuSwitch @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr) { + private var menuTitle: String? = null + @ColorInt + private var menuTitleColor: Int = 0 + private var menuTitleSize: Float = 0.toFloat() + + @DrawableRes + private var switchThumb: Int = 0 + @DrawableRes + private var switchTrack: Int = 0 + + private var leftMargin: Int = 0 + private var rightMargin: Int = 0 + + private var tv_title: TextView? = null + private var aSwitch: Switch? = null + private var checked = false + private var onCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null + + init { + getAttr(context, attrs, defStyleAttr) + initView() + } + + private fun initView() { + tv_title = TextView(context) + tv_title!!.text = menuTitle + tv_title!!.setTextColor(menuTitleColor) + tv_title!!.textSize = menuTitleSize + tv_title!!.setBackgroundResource(R.color.transparent) + val title_param = LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + title_param.addRule(CENTER_VERTICAL) + title_param.leftMargin = leftMargin + tv_title!!.layoutParams = title_param + addView(tv_title, title_param) + + aSwitch = Switch(context) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + aSwitch!!.setThumbResource(switchThumb) + aSwitch!!.setTrackResource(switchTrack) + } + val switch_param = LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + switch_param.addRule(ALIGN_PARENT_END) + } + switch_param.addRule(ALIGN_PARENT_RIGHT) + switch_param.addRule(CENTER_VERTICAL) + switch_param.rightMargin = rightMargin + addView(aSwitch, switch_param) + aSwitch!!.isChecked = checked + aSwitch!!.setOnCheckedChangeListener(onCheckedChangeListener) + } + + private fun getAttr(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + val ta = context.obtainStyledAttributes(attrs, R.styleable.LineMenuSwitch) + menuTitle = ta.getString(R.styleable.LineMenuSwitch_menu_title) + menuTitleSize = ta.getFloat(R.styleable.LineMenuSwitch_menu_title_size, DEFAULT_TEXT_SIZE.toFloat()) + menuTitleColor = ta.getColor( + R.styleable.LineMenuSwitch_menu_title_color, + resources.getColor(DEFAULT_TITLE_COLOR) + ) + leftMargin = ta.getDimensionPixelSize( + R.styleable.LineMenuSwitch_left_margin, + dip2px(context, DEFAULT_MARGIN) + ) + rightMargin = ta.getDimensionPixelOffset( + R.styleable.LineMenuSwitch_right_margin, + dip2px(context, DEFAULT_MARGIN) + ) + switchThumb = ta.getResourceId(R.styleable.LineMenuSwitch_switch_thumb, R.drawable.switch_thumb) + switchTrack = ta.getResourceId(R.styleable.LineMenuSwitch_switch_track, R.drawable.switch_track) + ta.recycle() + } + + fun setChecked(checked: Boolean) { + this.checked = checked + if (aSwitch != null) + aSwitch!!.isChecked = checked + } + + fun setOnCheckedChangeListener(onCheckedChangeListener: CompoundButton.OnCheckedChangeListener) { + this.onCheckedChangeListener = onCheckedChangeListener + if (aSwitch != null) + aSwitch!!.setOnCheckedChangeListener(onCheckedChangeListener) + } + + companion object { + + private val DEFAULT_TEXT_SIZE = 14 + private val DEFAULT_MARGIN = 13f + + private val DEFAULT_TITLE_COLOR = R.color.text_gray + } + + + /** + * 根据手机的分辨率从 dp 的单位 转成为 px(像素) + */ + fun dip2px(context: Context, dpValue: Float): Int { + val scale = context.resources.displayMetrics.density + return (dpValue * scale + 0.5f).toInt() + } + + /** + * 将px值转换为sp值,保证文字大小不变 + * + * @param pxValue (DisplayMetrics类中属性scaledDensity) + * @return + */ + fun px2sp(context: Context, pxValue: Float): Int { + val fontScale = context.resources.displayMetrics.scaledDensity + return (pxValue / fontScale + 0.5f).toInt() + } + + /** + * 将sp值转换为px值,保证文字大小不变 + * + * @param spValue (DisplayMetrics类中属性scaledDensity) + * @return + */ + fun sp2px(context: Context, spValue: Float): Int { + val fontScale = context.resources.displayMetrics.scaledDensity + return (spValue * fontScale + 0.5f).toInt() + } +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/NoneScrollViewPager.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/NoneScrollViewPager.kt new file mode 100644 index 0000000..bfb8e69 --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/other/NoneScrollViewPager.kt @@ -0,0 +1,58 @@ +package cn.qjm253.quick_android_custom_view.other + +import android.content.Context +import android.util.AttributeSet +import androidx.viewpager.widget.ViewPager +import android.view.MotionEvent + + + +/** + * @author SunnyQjm + * @date 19-7-14 下午7:52 + */ + +class NoneScrollViewPager(context: Context, attrs: AttributeSet? = null) : ViewPager(context, attrs) { + private var isScroll = false + init { + + } + + /** + * 是否拦截 + * 拦截:会走到自己的onTouchEvent方法里面来 + * 不拦截:事件传递给子孩子 + */ + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + // return false;//可行,不拦截事件, + // return true;//不行,孩子无法处理事件 + //return super.onInterceptTouchEvent(ev);//不行,会有细微移动 + return if (isScroll) { + super.onInterceptTouchEvent(ev) + } else { + false + } + } + + /** + * 是否消费事件 + * 消费:事件就结束 + * 不消费:往父控件传 + */ + override fun onTouchEvent(ev: MotionEvent): Boolean { + //return false;// 可行,不消费,传给父控件 + //return true;// 可行,消费,拦截事件 + //super.onTouchEvent(ev); //不行, + //虽然onInterceptTouchEvent中拦截了, + //但是如果viewpage里面子控件不是viewgroup,还是会调用这个方法. + return if (isScroll) { + super.onTouchEvent(ev) + } else { + true// 可行,消费,拦截事件 + } + } + + fun setScroll(scroll: Boolean) { + isScroll = scroll + } +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/tin_view/TinButton.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/tin_view/TinButton.kt new file mode 100644 index 0000000..dd1ef02 --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/tin_view/TinButton.kt @@ -0,0 +1,112 @@ +package cn.qjm253.quick_android_custom_view.tin_view + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import cn.qjm253.quick_android_custom_view.R +import cn.qjm253.quick_android_custom_view.custom_drawable_size_view.DrawableButton + +/** + * 这是一个 DrawableButton 的封装组件,在DrawableButton的基础上支持设置drawable的tinColor + * + * @see DrawableButton + * @author SunnyQjm + * @date 19-7-10 下午10:33 + */ +class TinButton @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.buttonStyle +): DrawableButton(context, attrs, defStyleAttr) { + companion object { + const val NONE = 0x0000 + const val LEFT = 0x0001 + const val TOP = 0x0002 + const val RIGHT = 0x0004 + const val BOTTOM = 0x0008 + const val ALL = 0x000f + } + + + private var tints: Int = TinTextView.NONE + private lateinit var mDrawables: Array + + init { + if (!isInEditMode) { + mDrawables = compoundDrawables + loadFromAttribute(context, attrs, defStyleAttr) + } + } + + /** + * 从布局文件中加载属性配置 + */ + private fun loadFromAttribute(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + attrs?.let { + val ta = context.obtainStyledAttributes(it, R.styleable.TinButton) + tints = ta.getInt(R.styleable.TinButton_tints, ALL) + ta.getColorStateList(R.styleable.TinButton_tinColor)?.let { tinColorStateList -> + updateDrawablesTinColor(tinColorStateList) + } + ta.recycle() + } + } + + /** + * 设置哪些方向的drawable需要设置tinColor + * + * @see TinButton.NONE -> 四个方向都不着色 + * @see TinButton.LEFT -> 左边drawable着色 + * @see TinButton.RIGHT -> 右边drawable着色 + * @see TinButton.TOP -> 顶部drawable着色 + * @see TinButton.BOTTOM -> 底部drawable着色 + * @see TinButton.ALL -> 四个方向都着色 + * + * 如果需要单独设置两个方向可以 => TinButton.LEFT or TinButton.RIGHT + * + */ + fun updateTints(tints: Int) { + if (tints < 0 || tints > 8) + return + this.tints = tints + } + + /** + * 设置tinColor的颜色 + */ + private fun updateDrawablesTinColor(colorStateList: ColorStateList) { + //设置icon + if (mDrawables[0] != null && (tints and TinTextView.LEFT) == TinTextView.LEFT) { + mDrawables[0] = DrawableCompat.wrap(mDrawables[0]!!.mutate()) + DrawableCompat.setTintList(mDrawables[0]!!, colorStateList) + } + + if (mDrawables[1] != null && (tints and TinTextView.TOP) == TinTextView.TOP) { + mDrawables[1] = DrawableCompat.wrap(mDrawables[1]!!.mutate()) + DrawableCompat.setTintList(mDrawables[1]!!, colorStateList) + } + + if (mDrawables[2] != null && (tints and TinTextView.RIGHT) == TinTextView.RIGHT) { + mDrawables[2] = DrawableCompat.wrap(mDrawables[2]!!.mutate()) + DrawableCompat.setTintList(mDrawables[2]!!, colorStateList) + } + + if (mDrawables[3] != null && (tints and TinTextView.BOTTOM) == TinTextView.BOTTOM) { + mDrawables[3] = DrawableCompat.wrap(mDrawables[3]!!.mutate()) + DrawableCompat.setTintList(mDrawables[3]!!, colorStateList) + } + setCompoundDrawables(mDrawables[0], mDrawables[1], mDrawables[2], mDrawables[3]) + } + + /** + * 设置tinColor的颜色 + */ + fun updateDrawablesTinColor(@ColorRes color: Int) { + ContextCompat.getColorStateList(context, color)?.let { + updateDrawablesTinColor(it) + } + } + +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/tin_view/TinEditText.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/tin_view/TinEditText.kt new file mode 100644 index 0000000..2fb7053 --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/tin_view/TinEditText.kt @@ -0,0 +1,146 @@ +package cn.qjm253.quick_android_custom_view.tin_view + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.View +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import cn.qjm253.quick_android_custom_view.R +import cn.qjm253.quick_android_custom_view.custom_drawable_size_view.DrawableEditText + +/** + * 这是一个 DrawableEditText 的封装组件,在 DrawableEditText 的基础上支持设置drawable的tinColor + * + * @see DrawableEditText + * @author SunnyQjm + * @date 19-7-10 下午10:15 + */ + +open class TinEditText @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle +) : DrawableEditText(context, attrs, defStyleAttr), View.OnFocusChangeListener { + + + companion object { + const val NONE = 0x0000 + const val LEFT = 0x0001 + const val TOP = 0x0002 + const val RIGHT = 0x0004 + const val BOTTOM = 0x0008 + const val ALL = 0x000f + } + + + private var tints: Int = NONE + + private var tinColor: ColorStateList? = null + private var focusedTinColor: ColorStateList? = null + + protected lateinit var mDrawables: Array + + init { + + if (!isInEditMode) { + mDrawables = compoundDrawables + loadFromAttribute(context, attrs, defStyleAttr) + this.onFocusChangeListener = this + } + } + + /** + * 从布局文件中加载属性配置 + */ + private fun loadFromAttribute(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + attrs?.let { + val ta = context.obtainStyledAttributes(it, R.styleable.TinEditText) + tints = ta.getInt(R.styleable.TinEditText_tints, ALL) + tinColor = ta.getColorStateList(R.styleable.TinEditText_tinColor) + focusedTinColor = ta.getColorStateList(R.styleable.TinEditText_focusedTinColor) + ta.recycle() + } + + // 根绝当前是否聚焦,修改tinColor + onFocusChange(this, isFocused) + } + + /** + * 设置哪些方向的drawable需要设置tinColor + * + * @see TinEditText.NONE -> 四个方向都不着色 + * @see TinEditText.LEFT -> 左边drawable着色 + * @see TinEditText.RIGHT -> 右边drawable着色 + * @see TinEditText.TOP -> 顶部drawable着色 + * @see TinEditText.BOTTOM -> 底部drawable着色 + * @see TinEditText.ALL -> 四个方向都着色 + * + * 如果需要单独设置两个方向可以 => TinEditText.LEFT or TinEditText.RIGHT + * + */ + fun updateTints(tints: Int) { + if (tints < 0 || tints > 8) + return + this.tints = tints + } + + /** + * 设置tinColor的颜色 + */ + private fun updateDrawablesTinColor(colorStateList: ColorStateList?) { + colorStateList?.let { + //设置icon + if (mDrawables[0] != null && (tints and LEFT) == LEFT) { + mDrawables[0] = DrawableCompat.wrap(mDrawables[0]!!.mutate()) + DrawableCompat.setTintList(mDrawables[0]!!, colorStateList) + } + + if (mDrawables[1] != null && (tints and TinTextView.TOP) == TinTextView.TOP) { + mDrawables[1] = DrawableCompat.wrap(mDrawables[1]!!.mutate()) + DrawableCompat.setTintList(mDrawables[1]!!, colorStateList) + } + + if (mDrawables[2] != null && (tints and RIGHT) == RIGHT) { + mDrawables[2] = DrawableCompat.wrap(mDrawables[2]!!.mutate()) + DrawableCompat.setTintList(mDrawables[2]!!, colorStateList) + } + + if (mDrawables[3] != null && (tints and BOTTOM) == BOTTOM) { + mDrawables[3] = DrawableCompat.wrap(mDrawables[3]!!.mutate()) + DrawableCompat.setTintList(mDrawables[3]!!, colorStateList) + } + setCompoundDrawables(mDrawables[0], mDrawables[1], mDrawables[2], mDrawables[3]) + } + } + + + /** + * 设置tinColor的颜色 + */ + fun updateDrawablesTinColor(@ColorRes color: Int) { + this.tinColor = ContextCompat.getColorStateList(context, color) + onFocusChange(this, isFocused) + } + + + /** + * 更新聚焦状态下 drawable 的颜色 + */ + fun updateDrawablesFocusedTinColor(@ColorRes color: Int) { + this.focusedTinColor = ContextCompat.getColorStateList(context, color) + onFocusChange(this, isFocused) + } + + + /** + * 处理EditText焦点变化的时候修改 drawable 的 tinColor + */ + override fun onFocusChange(v: View?, hasFocus: Boolean) { + if (hasFocus) { + updateDrawablesTinColor(focusedTinColor) + } else { + updateDrawablesTinColor(tinColor) + } + } +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/tin_view/TinImageView.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/tin_view/TinImageView.kt new file mode 100644 index 0000000..6bd69b2 --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/tin_view/TinImageView.kt @@ -0,0 +1,65 @@ +package cn.qjm253.quick_android_custom_view.tin_view + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import androidx.annotation.ColorRes +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import cn.qjm253.quick_android_custom_view.R + +/** + * 这是一个 DrawableButton 的封装组件,在AppCompatImageView的基础上支持设置drawable的tinColor + * + * @author SunnyQjm + * @date 19-7-11 下午4:46 + */ + +class TinImageView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : AppCompatImageView(context, attrs, defStyleAttr) { + + private var mDrawable: Drawable? = drawable + + init { + loadFromAttribute(context, attrs, defStyleAttr) + } + + /** + * 从布局文件中加载属性配置 + */ + private fun loadFromAttribute(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + attrs?.let { + val ta = context.obtainStyledAttributes(it, R.styleable.TinImageView) + ta.getColorStateList(R.styleable.TinImageView_tinColor)?.let { tinColorStateList -> + updateDrawablesTinColor(tinColorStateList) + } + ta.recycle() + } + } + + /** + * 设置tinColor的颜色 + */ + fun updateDrawablesTinColor(@ColorRes color: Int) { + updateDrawablesTinColor(ContextCompat.getColorStateList(context, color)) + } + + /** + * 设置tinColor的颜色 + */ + private fun updateDrawablesTinColor(colorStateList: ColorStateList?) { + if (mDrawable == null) { + mDrawable = drawable + } + colorStateList?.let { + mDrawable?.let { _mDrawable -> + mDrawable = DrawableCompat.wrap(_mDrawable.mutate()) + DrawableCompat.setTintList(mDrawable!!, colorStateList) + setImageDrawable(mDrawable) + } + } + } +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/tin_view/TinTextView.kt b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/tin_view/TinTextView.kt new file mode 100644 index 0000000..57fa80d --- /dev/null +++ b/quick_android_custom_view/src/main/java/cn/qjm253/quick_android_custom_view/tin_view/TinTextView.kt @@ -0,0 +1,114 @@ +package cn.qjm253.quick_android_custom_view.tin_view + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import cn.qjm253.quick_android_custom_view.R +import cn.qjm253.quick_android_custom_view.custom_drawable_size_view.DrawableTextView + +/** + * 这是一个 DrawableTextView 的封装组件,在DrawableTextView的基础上支持设置drawable的tinColor + * + * @see DrawableTextView + * @author SunnyQjm + * @date 19-7-10 下午9:12 + */ + +class TinTextView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.textViewStyle +) : DrawableTextView(context, attrs, defStyleAttr) { + companion object { + const val NONE = 0x0000 + const val LEFT = 0x0001 + const val TOP = 0x0002 + const val RIGHT = 0x0004 + const val BOTTOM = 0x0008 + const val ALL = 0x000f + } + + + private var tints: Int = NONE + private lateinit var mDrawables: Array + + init { + if (!isInEditMode) { + mDrawables = compoundDrawables + loadFromAttribute(context, attrs, defStyleAttr) + } + } + + /** + * 从布局文件中加载属性配置 + */ + private fun loadFromAttribute(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + attrs?.let { + val ta = context.obtainStyledAttributes(it, R.styleable.TinTextView) + tints = ta.getInt(R.styleable.TinTextView_tints, ALL) + ta.getColorStateList(R.styleable.TinTextView_tinColor)?.let { tinColorStateList -> + updateDrawablesTinColor(tinColorStateList) + } + ta.recycle() + } + } + + + /** + * 设置哪些方向的drawable需要设置tinColor + * + * @see TinTextView.NONE -> 四个方向都不着色 + * @see TinTextView.LEFT -> 左边drawable着色 + * @see TinTextView.RIGHT -> 右边drawable着色 + * @see TinTextView.TOP -> 顶部drawable着色 + * @see TinTextView.BOTTOM -> 底部drawable着色 + * @see TinTextView.ALL -> 四个方向都着色 + * + * 如果需要单独设置两个方向可以 => TinTextView.LEFT or TinTextView.RIGHT + * + */ + fun updateTints(tints: Int) { + if (tints < 0 || tints > 8) + return + this.tints = tints + } + + /** + * 设置tinColor的颜色 + */ + private fun updateDrawablesTinColor(colorStateList: ColorStateList) { + //设置icon + if (mDrawables[0] != null && (tints and LEFT) == LEFT) { + mDrawables[0] = DrawableCompat.wrap(mDrawables[0]!!.mutate()) + DrawableCompat.setTintList(mDrawables[0]!!, colorStateList) + } + + if (mDrawables[1] != null && (tints and TOP) == TOP) { + mDrawables[1] = DrawableCompat.wrap(mDrawables[1]!!.mutate()) + DrawableCompat.setTintList(mDrawables[1]!!, colorStateList) + } + + if (mDrawables[2] != null && (tints and RIGHT) == RIGHT) { + mDrawables[2] = DrawableCompat.wrap(mDrawables[2]!!.mutate()) + DrawableCompat.setTintList(mDrawables[2]!!, colorStateList) + } + + if (mDrawables[3] != null && (tints and BOTTOM) == BOTTOM) { + mDrawables[3] = DrawableCompat.wrap(mDrawables[3]!!.mutate()) + DrawableCompat.setTintList(mDrawables[3]!!, colorStateList) + } + setCompoundDrawables(mDrawables[0], mDrawables[1], mDrawables[2], mDrawables[3]) + } + + + /** + * 设置tinColor的颜色 + */ + fun updateDrawablesTinColor(@ColorRes color: Int) { + ContextCompat.getColorStateList(context, color)?.let { + updateDrawablesTinColor(it) + } + } +} \ No newline at end of file diff --git a/quick_android_custom_view/src/main/res/drawable/blue_thumb.xml b/quick_android_custom_view/src/main/res/drawable/blue_thumb.xml new file mode 100644 index 0000000..8a37987 --- /dev/null +++ b/quick_android_custom_view/src/main/res/drawable/blue_thumb.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/quick_android_custom_view/src/main/res/drawable/blue_track.xml b/quick_android_custom_view/src/main/res/drawable/blue_track.xml new file mode 100644 index 0000000..7111385 --- /dev/null +++ b/quick_android_custom_view/src/main/res/drawable/blue_track.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/quick_android_custom_view/src/main/res/drawable/gray_thumb.xml b/quick_android_custom_view/src/main/res/drawable/gray_thumb.xml new file mode 100644 index 0000000..872df99 --- /dev/null +++ b/quick_android_custom_view/src/main/res/drawable/gray_thumb.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/quick_android_custom_view/src/main/res/drawable/gray_track.xml b/quick_android_custom_view/src/main/res/drawable/gray_track.xml new file mode 100644 index 0000000..ade52a6 --- /dev/null +++ b/quick_android_custom_view/src/main/res/drawable/gray_track.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/quick_android_custom_view/src/main/res/drawable/icon_arrow.png b/quick_android_custom_view/src/main/res/drawable/icon_arrow.png new file mode 100644 index 0000000..d6d2924 Binary files /dev/null and b/quick_android_custom_view/src/main/res/drawable/icon_arrow.png differ diff --git a/quick_android_custom_view/src/main/res/drawable/menu.png b/quick_android_custom_view/src/main/res/drawable/menu.png new file mode 100644 index 0000000..ac272f4 Binary files /dev/null and b/quick_android_custom_view/src/main/res/drawable/menu.png differ diff --git a/quick_android_custom_view/src/main/res/drawable/switch_thumb.xml b/quick_android_custom_view/src/main/res/drawable/switch_thumb.xml new file mode 100644 index 0000000..4f619f7 --- /dev/null +++ b/quick_android_custom_view/src/main/res/drawable/switch_thumb.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/quick_android_custom_view/src/main/res/drawable/switch_track.xml b/quick_android_custom_view/src/main/res/drawable/switch_track.xml new file mode 100644 index 0000000..9453a17 --- /dev/null +++ b/quick_android_custom_view/src/main/res/drawable/switch_track.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/quick_android_custom_view/src/main/res/values/custom_drawable_size_view.xml b/quick_android_custom_view/src/main/res/values/custom_drawable_size_view.xml new file mode 100644 index 0000000..0e4145b --- /dev/null +++ b/quick_android_custom_view/src/main/res/values/custom_drawable_size_view.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quick_android_custom_view/src/main/res/values/line_menu.xml b/quick_android_custom_view/src/main/res/values/line_menu.xml new file mode 100644 index 0000000..7e3db22 --- /dev/null +++ b/quick_android_custom_view/src/main/res/values/line_menu.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quick_android_custom_view/src/main/res/values/quick_android_custom_view_color.xml b/quick_android_custom_view/src/main/res/values/quick_android_custom_view_color.xml new file mode 100644 index 0000000..f93779b --- /dev/null +++ b/quick_android_custom_view/src/main/res/values/quick_android_custom_view_color.xml @@ -0,0 +1,13 @@ + + + #161923 + #eee + #bbb + #fff + #00ffffff + #333333 + #000 + #f00 + + #dddddd + \ No newline at end of file diff --git a/quick_android_custom_view/src/main/res/values/quick_android_custom_view_dimen.xml b/quick_android_custom_view/src/main/res/values/quick_android_custom_view_dimen.xml new file mode 100644 index 0000000..e73e92e --- /dev/null +++ b/quick_android_custom_view/src/main/res/values/quick_android_custom_view_dimen.xml @@ -0,0 +1,6 @@ + + + 26dp + 13dp + 35dp + \ No newline at end of file diff --git a/quick_android_custom_view/src/main/res/values/strings.xml b/quick_android_custom_view/src/main/res/values/strings.xml new file mode 100644 index 0000000..8542005 --- /dev/null +++ b/quick_android_custom_view/src/main/res/values/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/quick_android_custom_view/src/main/res/values/tin_view.xml b/quick_android_custom_view/src/main/res/values/tin_view.xml new file mode 100644 index 0000000..82b4828 --- /dev/null +++ b/quick_android_custom_view/src/main/res/values/tin_view.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quick_android_custom_view/src/test/java/cn/qjm253/quick_android_custom_view/ExampleUnitTest.java b/quick_android_custom_view/src/test/java/cn/qjm253/quick_android_custom_view/ExampleUnitTest.java new file mode 100644 index 0000000..0f21fc1 --- /dev/null +++ b/quick_android_custom_view/src/test/java/cn/qjm253/quick_android_custom_view/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package cn.qjm253.quick_android_custom_view; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/quick_android_easy_bar/.gitignore b/quick_android_easy_bar/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/quick_android_easy_bar/.gitignore @@ -0,0 +1 @@ +/build diff --git a/quick_android_easy_bar/build.gradle b/quick_android_easy_bar/build.gradle new file mode 100644 index 0000000..4c207f8 --- /dev/null +++ b/quick_android_easy_bar/build.gradle @@ -0,0 +1,38 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +android { + compileSdkVersion compile_sdk_version + + defaultConfig { + minSdkVersion min_sdk_version + targetSdkVersion target_sdk_version + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "androidx.appcompat:appcompat:${appcompact_version}" + implementation "androidx.core:core-ktx:${core_ktx_version}" + testImplementation "junit:junit:${junit_version}" + + implementation project(path: ':quick_android_base') + implementation project(path: ':quick_android_custom_view') + + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/quick_android_easy_bar/consumer-rules.pro b/quick_android_easy_bar/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/quick_android_easy_bar/proguard-rules.pro b/quick_android_easy_bar/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/quick_android_easy_bar/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/quick_android_easy_bar/src/androidTest/java/cn/qjm253/quick_android_easy_bar/ExampleInstrumentedTest.kt b/quick_android_easy_bar/src/androidTest/java/cn/qjm253/quick_android_easy_bar/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..e54a232 --- /dev/null +++ b/quick_android_easy_bar/src/androidTest/java/cn/qjm253/quick_android_easy_bar/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package cn.qjm253.quick_android_easy_bar + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("cn.qjm253.quick_android_easy_bar.test", appContext.packageName) + } +} diff --git a/quick_android_easy_bar/src/main/AndroidManifest.xml b/quick_android_easy_bar/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6a731b4 --- /dev/null +++ b/quick_android_easy_bar/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/quick_android_easy_bar/src/main/java/cn/qjm253/quick_android_easy_bar/EasyBar.kt b/quick_android_easy_bar/src/main/java/cn/qjm253/quick_android_easy_bar/EasyBar.kt new file mode 100644 index 0000000..4ea1087 --- /dev/null +++ b/quick_android_easy_bar/src/main/java/cn/qjm253/quick_android_easy_bar/EasyBar.kt @@ -0,0 +1,354 @@ +package cn.qjm253.quick_android_easy_bar + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.Build +import android.text.TextUtils +import android.util.AttributeSet +import android.util.TypedValue +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.Dimension +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import cn.qjm253.quick_android_base.util.DisplayUtils +import cn.qjm253.quick_android_custom_view.tin_view.TinImageView + + +/** + * Created by Sunny on 2017/8/17 0017. + */ + +class EasyBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr) { + private var title: String? = null + @Dimension + private var titleSize: Int = 0 + @ColorInt + private var titleColor: Int = 0 + @DrawableRes + private var leftIcon: Int = 0 + @DrawableRes + private var rightIcon: Int = 0 + @Dimension + private var iconSize: Int = 0 + @Dimension + private var iconMargin: Int = 0 + + private var leftText: String = "" + private var rightText: String = "" + private var displayMode: Mode = Mode.TEXT + + + private lateinit var imgLeft: TinImageView + private lateinit var imgRight: TinImageView + private lateinit var tvTitle: TextView + private lateinit var tvLeftText: TextView + private lateinit var tvRightText: TextView + private var mListener: OnEasyBarClickListener? = null + + init { + initAttr(context, attrs) + initView(context) + } + + @SuppressLint("ObsoleteSdkInt") + private fun initView(context: Context) { + + //left icon + imgLeft = TinImageView(context) + imgLeft.setImageResource(leftIcon) + imgLeft.scaleType = ImageView.ScaleType.FIT_CENTER + val imgLeftParam = RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + imgLeftParam.addRule(RelativeLayout.CENTER_VERTICAL) + imgLeftParam.addRule(RelativeLayout.ALIGN_PARENT_LEFT) + imgLeftParam.width = iconSize + imgLeftParam.height = iconSize + imgLeft.layoutParams = imgLeftParam + imgLeft.setPadding(iconMargin, iconMargin, iconMargin, iconMargin) + addView(imgLeft, imgLeftParam) + + imgLeft.setOnClickListener { v -> + mListener?.onLeftIconClick(v) + } + + //center title + tvTitle = TextView(context) + tvTitle.ellipsize = TextUtils.TruncateAt.END + tvTitle.setSingleLine() + tvTitle.text = title + tvTitle.textSize = titleSize.toFloat() + tvTitle.setTextColor(titleColor) + val tvTitleParam = RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + val titleMargin = iconSize + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + tvTitleParam.marginStart = titleMargin + tvTitleParam.marginEnd = titleMargin + } else { + tvTitleParam.leftMargin = titleMargin + tvTitleParam.rightMargin = titleMargin + } + tvTitleParam.addRule(RelativeLayout.CENTER_IN_PARENT) + tvTitle.layoutParams = tvTitleParam + addView(tvTitle, tvTitleParam) + + //left text + tvLeftText = TextView(context) + tvLeftText.textSize = titleSize.toFloat() + tvLeftText.setTextColor(titleColor) + val tvLeftTextParam = RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + tvLeftTextParam.addRule(RelativeLayout.ALIGN_PARENT_LEFT) + tvLeftTextParam.addRule(RelativeLayout.CENTER_VERTICAL) + tvLeftTextParam.leftMargin = iconMargin + tvLeftText.layoutParams = tvLeftTextParam + addView(tvLeftText) + tvLeftText.text = leftText + tvLeftText.visibility = View.INVISIBLE + tvLeftText.setOnClickListener { v -> + mListener?.onLeftIconClick(v) + } + + + //right text + tvRightText = TextView(context) + tvRightText.textSize = titleSize.toFloat() + tvRightText.setTextColor(titleColor) + val tvRightTextParam = RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + tvRightTextParam.addRule(RelativeLayout.ALIGN_PARENT_RIGHT) + tvRightTextParam.addRule(RelativeLayout.CENTER_VERTICAL) + tvRightTextParam.rightMargin = iconMargin + tvRightText.layoutParams = tvRightTextParam + addView(tvRightText) + tvRightText.text = rightText + tvRightText.visibility = View.INVISIBLE + tvRightText.setOnClickListener { v -> + mListener?.onRightIconClick(v) + } + + //right icon + imgRight = TinImageView(context) + imgRight.setImageResource(rightIcon) + imgRight.scaleType = ImageView.ScaleType.FIT_CENTER + val imgRightParam = RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + imgRightParam.addRule(RelativeLayout.CENTER_VERTICAL) + imgRightParam.addRule(RelativeLayout.ALIGN_PARENT_RIGHT) + imgRightParam.width = iconSize + imgRightParam.height = iconSize + imgRight.layoutParams = imgRightParam + imgRight.setPadding(iconMargin, iconMargin, iconMargin, iconMargin) + addView(imgRight, imgRightParam) + + imgRight.visibility = View.INVISIBLE + imgRight.setOnClickListener { v -> + mListener?.onRightIconClick(v) + } + + setDisplayMode(displayMode) + } + + private fun initAttr(context: Context, attrs: AttributeSet?) { + val ta = context.obtainStyledAttributes(attrs, R.styleable.EasyBar) + title = ta.getString(R.styleable.EasyBar_title) + titleSize = DisplayUtils.px2sp( + context, + ta.getDimensionPixelSize( + R.styleable.EasyBar_title_size, + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + 14f, + resources.displayMetrics + ).toInt() + ).toFloat() + ) + titleColor = + ta.getColor(R.styleable.EasyBar_title_color, resources.getColor(R.color.text_gray)) + leftIcon = ta.getResourceId(R.styleable.EasyBar_left_icon, R.drawable.back) +// rightIcon = ta.getResourceId(R.styleable.EasyBar_right_icon, R.drawable.back) + iconSize = ta.getDimensionPixelSize( + R.styleable.EasyBar_icon_size, + DisplayUtils.dip2px(context, 24f) + ) + iconMargin = ta.getDimensionPixelOffset( + R.styleable.EasyBar_icon_margin, + DisplayUtils.dip2px(context, 12f) + ) + leftText = ta.getString(R.styleable.EasyBar_left_text).toString() + rightText = ta.getString(R.styleable.EasyBar_right_text).toString() + val mode = ta.getInt(R.styleable.EasyBar_displayMode, 0) + displayMode = when (mode) { + 0 -> Mode.ICON + 1 -> Mode.TEXT + else -> Mode.ICON + } + ta.recycle() + } + + fun setTitle(title: String) { + this.title = title + tvTitle.text = title + } + + fun setTitle(@StringRes titleRes: Int) { + this.title = context.getString(titleRes) + tvTitle.setText(titleRes) + } + + fun setLeftText(text: String) { + this.leftText = text + tvLeftText.text = text + } + + fun setRightText(text: String) { + this.rightText = text + tvRightText.text = text + } + + fun setOnEasyBarClickListener(listener: OnEasyBarClickListener) { + this.mListener = listener + } + + + fun setRightIcon(drawable: Drawable) { + imgRight.setImageDrawable(drawable) + } + + fun setRightIcon(bitmap: Bitmap) { + imgRight.setImageBitmap(bitmap) + } + + fun setRightIcon(@DrawableRes res: Int) { + imgRight.setImageResource(res) + } + + fun getRightIcon(): TinImageView? { + return imgRight + } + + fun getRightText(): String? { + return tvRightText.text.toString() + } + + fun setLeftIcon(drawable: Drawable) { + imgLeft.setImageDrawable(drawable) + } + + fun setLeftIcon(bitmap: Bitmap) { + imgLeft.setImageBitmap(bitmap) + } + + fun setLeftIcon(@DrawableRes res: Int) { + imgLeft.setImageResource(res) + println("drawable: ${imgLeft.drawable}") + println("drawable state: ${imgLeft.drawableState}") + println("drawing time: ${imgLeft.drawingTime}") + } + + fun getLeftIcon(): TinImageView? { + return imgLeft + } + + interface OnEasyBarClickListener { + /** + * 如果是图标模式,则响应左图标点击,否则响应左文字点击 + * + * @param view + */ + fun onLeftIconClick(view: View) + + /** + * 如果是图标模式,则响应右图标点击,否则响应右文字点击 + * + * @param view + */ + fun onRightIconClick(view: View) + } + + + enum class Mode { + ICON, TEXT, ICON_TEXT, TEXT_ICON, ICON_, TEXT_, _ICON, _TEXT, NONE + } + + fun setDisplayMode(mode: Mode?) { + when (mode) { + Mode.ICON -> { + tvLeftText.visibility = View.INVISIBLE + tvRightText.visibility = View.INVISIBLE + imgLeft.visibility = View.VISIBLE + imgRight.visibility = View.VISIBLE + + } + Mode.TEXT -> { + tvLeftText.visibility = View.VISIBLE + tvRightText.visibility = View.VISIBLE + imgLeft.visibility = View.INVISIBLE + imgRight.visibility = View.INVISIBLE + } + Mode.ICON_TEXT -> { + tvLeftText.visibility = View.INVISIBLE + tvRightText.visibility = View.VISIBLE + imgLeft.visibility = View.VISIBLE + imgRight.visibility = View.INVISIBLE + } + Mode.TEXT_ICON -> { + tvLeftText.visibility = View.VISIBLE + tvRightText.visibility = View.INVISIBLE + imgLeft.visibility = View.INVISIBLE + imgRight.visibility = View.VISIBLE + } + Mode.TEXT_ -> { + tvLeftText.visibility = View.VISIBLE + tvRightText.visibility = View.INVISIBLE + imgLeft.visibility = View.INVISIBLE + imgRight.visibility = View.INVISIBLE + } + Mode.ICON_ -> { + tvLeftText.visibility = View.INVISIBLE + tvRightText.visibility = View.INVISIBLE + imgLeft.visibility = View.VISIBLE + imgRight.visibility = View.INVISIBLE + } + Mode._ICON -> { + tvLeftText.visibility = View.INVISIBLE + tvRightText.visibility = View.INVISIBLE + imgLeft.visibility = View.INVISIBLE + imgRight.visibility = View.VISIBLE + } + Mode._TEXT -> { + tvLeftText.visibility = View.INVISIBLE + tvRightText.visibility = View.VISIBLE + imgLeft.visibility = View.INVISIBLE + imgRight.visibility = View.INVISIBLE + } + Mode.NONE -> { + tvLeftText.visibility = View.INVISIBLE + tvRightText.visibility = View.INVISIBLE + imgLeft.visibility = View.INVISIBLE + imgRight.visibility = View.INVISIBLE + } + } + } +} diff --git a/quick_android_easy_bar/src/main/java/cn/qjm253/quick_android_easy_bar/EasyBarExtensions.kt b/quick_android_easy_bar/src/main/java/cn/qjm253/quick_android_easy_bar/EasyBarExtensions.kt new file mode 100644 index 0000000..a1bd07a --- /dev/null +++ b/quick_android_easy_bar/src/main/java/cn/qjm253/quick_android_easy_bar/EasyBarExtensions.kt @@ -0,0 +1,89 @@ +package cn.qjm253.quick_android_easy_bar + + +import android.annotation.SuppressLint +import android.view.View +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes + +/** + * Created by sunny on 18-1-25. + */ + +class EasyBarParams( + val mode: EasyBar.Mode = EasyBar.Mode.ICON_, + val title: String = "", + val leftCallback: (v: View) -> Unit = {}, + val rightCallback: (v: View) -> Unit = { + println("empty right") + }, @DrawableRes val leftRes: Int = R.drawable.back, + @DrawableRes val rightRes: Int = 0, + val leftText: String = "", + val rightText: String = "", @StringRes val titleRes: Int = -1, + val isCoverCallback: Boolean = true, + @ColorRes + val tinColor: Int = -1 +) { +} + +fun EasyBar.init(easyBarParams: EasyBarParams) { + init( + easyBarParams.mode, + easyBarParams.title, + easyBarParams.leftCallback, + easyBarParams.rightCallback, + easyBarParams.leftRes, + easyBarParams.rightRes, + easyBarParams.leftText, + easyBarParams.rightText, + easyBarParams.titleRes, + easyBarParams.isCoverCallback, + easyBarParams.tinColor + ) +} + +/** + * 标题栏初始化 + */ +@SuppressLint("ResourceType") +fun EasyBar.init( + mode: EasyBar.Mode = EasyBar.Mode.ICON_, + title: String = "", + leftCallback: (v: View) -> Unit = {}, + rightCallback: (v: View) -> Unit = { + println("empty right") + }, @DrawableRes leftRes: Int = R.drawable.back, + @DrawableRes rightRes: Int = 0, + leftText: String = "", + rightText: String = "", @StringRes titleRes: Int = -1, + isCoverCallback: Boolean = true, + @ColorRes tinColor: Int = -1 + +) { + this.setDisplayMode(mode) + if (titleRes > 0) + this.setTitle(titleRes) + else + this.setTitle(title) + this.setRightIcon(rightRes) + this.setLeftText(leftText) + this.setRightText(rightText) + this.setLeftIcon(leftRes) + if (tinColor > 0) { + this.getLeftIcon()?.updateDrawablesTinColor(tinColor) + this.getRightIcon()?.updateDrawablesTinColor(tinColor) + } + if (isCoverCallback) + this.setOnEasyBarClickListener(object : EasyBar.OnEasyBarClickListener { + override fun onLeftIconClick(view: View) { + leftCallback(view) + } + + override fun onRightIconClick(view: View) { + rightCallback.invoke(view) + } + + }) +} \ No newline at end of file diff --git a/quick_android_easy_bar/src/main/res/drawable/back.png b/quick_android_easy_bar/src/main/res/drawable/back.png new file mode 100644 index 0000000..cab12e2 Binary files /dev/null and b/quick_android_easy_bar/src/main/res/drawable/back.png differ diff --git a/quick_android_easy_bar/src/main/res/values/colors.xml b/quick_android_easy_bar/src/main/res/values/colors.xml new file mode 100644 index 0000000..b779011 --- /dev/null +++ b/quick_android_easy_bar/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #333333 + \ No newline at end of file diff --git a/quick_android_easy_bar/src/main/res/values/quick_android_easy_bar_attrs.xml b/quick_android_easy_bar/src/main/res/values/quick_android_easy_bar_attrs.xml new file mode 100644 index 0000000..8268eda --- /dev/null +++ b/quick_android_easy_bar/src/main/res/values/quick_android_easy_bar_attrs.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quick_android_easy_bar/src/main/res/values/strings.xml b/quick_android_easy_bar/src/main/res/values/strings.xml new file mode 100644 index 0000000..8542005 --- /dev/null +++ b/quick_android_easy_bar/src/main/res/values/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/quick_android_easy_bar/src/test/java/cn/qjm253/quick_android_easy_bar/ExampleUnitTest.kt b/quick_android_easy_bar/src/test/java/cn/qjm253/quick_android_easy_bar/ExampleUnitTest.kt new file mode 100644 index 0000000..3d28083 --- /dev/null +++ b/quick_android_easy_bar/src/test/java/cn/qjm253/quick_android_easy_bar/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package cn.qjm253.quick_android_easy_bar + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/quick_android_file_picker/.gitignore b/quick_android_file_picker/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/quick_android_file_picker/.gitignore @@ -0,0 +1 @@ +/build diff --git a/quick_android_file_picker/README.md b/quick_android_file_picker/README.md new file mode 100644 index 0000000..332d2ff --- /dev/null +++ b/quick_android_file_picker/README.md @@ -0,0 +1,67 @@ +> ## quick_android_file_picker 模块说明文档 +> +> 本模块简单封装了打开系统的文件管理器选择文件的功能,并返回文件的原始Uri以及经过处理后文件的绝对路径。 +> +> 通过RxJava用简单的链式调用即可获得选择文件的结果 + +- 返回数据结构 + + ```kotlin + data class FilePickerResult ( + val path: String, // 文件的绝对路径 + val uri: Uri // 文件的Uri + ) + ``` + +- ### 使用示例 + + - 本接口需要读取文件的权限 => `READ_EXTERNAL_STORAGE`,权限的获取可以参考本库的另一个模块 [quick_android_rx_permission模块文档](https://github.com/SunnyQjm/quickandroid/tree/master/quick_android_rx_permission) + + - 包含权限请求的使用示例: + + ps: `checkMyPermission` 为 [quick_android_base模块文档](https://github.com/SunnyQjm/quickandroid/tree/master/quick_android_base) 模块提供的用于检查权限是否存在的 `Context` 类的扩展方法 + + ```kotlin + QuickAndroidFilePicker + .create(this) + .request( + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE + ) + ) + .subscribe { + if (it.granted) { + checkMyPermission(Manifest.permission.READ_EXTERNAL_STORAGE, { + QuickAndroid + .createFilePicker(this) + .pickerFile() + .subscribe { fpr -> + toast(fpr.path) + } + }) + } + } + + ////////////////////////////////////////////////////////////// + //// 使用 QuickAndroid 扩展的用法 + ////////////////////////////////////////////////////////////// + QuickAndroid + .createRxPermission(this) + .request( + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE + ) + ) + .subscribe { + if (it.granted) { + checkMyPermission(Manifest.permission.READ_EXTERNAL_STORAGE, { + QuickAndroid + .createFilePicker(this) + .pickerFile() + .subscribe { fpr -> + toast(fpr.path) + } + }) + } + } + ``` \ No newline at end of file diff --git a/quick_android_file_picker/build.gradle b/quick_android_file_picker/build.gradle new file mode 100644 index 0000000..ae1af08 --- /dev/null +++ b/quick_android_file_picker/build.gradle @@ -0,0 +1,45 @@ +apply plugin: 'com.android.library' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion compile_sdk_version + + defaultConfig { + minSdkVersion min_sdk_version + targetSdkVersion target_sdk_version + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "androidx.appcompat:appcompat:${appcompact_version}" + testImplementation "junit:junit:${junit_version}" + androidTestImplementation "androidx.test:runner:${test_runner_version}" + androidTestImplementation "androidx.test.espresso:espresso-core:${espresso_core_version}" + + implementation project(path: ':quick_android_base') +} diff --git a/quick_android_file_picker/consumer-rules.pro b/quick_android_file_picker/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/quick_android_file_picker/proguard-rules.pro b/quick_android_file_picker/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/quick_android_file_picker/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/quick_android_file_picker/src/androidTest/java/com/example/quick_android_file_picker/ExampleInstrumentedTest.kt b/quick_android_file_picker/src/androidTest/java/com/example/quick_android_file_picker/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..2bbecd9 --- /dev/null +++ b/quick_android_file_picker/src/androidTest/java/com/example/quick_android_file_picker/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.quick_android_file_picker + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.quick_android_file_picker.test", appContext.packageName) + } +} diff --git a/quick_android_file_picker/src/main/AndroidManifest.xml b/quick_android_file_picker/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fac2a43 --- /dev/null +++ b/quick_android_file_picker/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/quick_android_file_picker/src/main/java/com/example/quick_android_file_picker/QuickAndroidFilePicker.kt b/quick_android_file_picker/src/main/java/com/example/quick_android_file_picker/QuickAndroidFilePicker.kt new file mode 100644 index 0000000..4a13da5 --- /dev/null +++ b/quick_android_file_picker/src/main/java/com/example/quick_android_file_picker/QuickAndroidFilePicker.kt @@ -0,0 +1,50 @@ +package com.example.quick_android_file_picker + +import androidx.annotation.RequiresPermission +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import cn.qjm253.quick_android_base.QuickAndroid +import cn.qjm253.quick_android_base.extensions.getLazySingleton +import com.example.quick_android_file_picker.model.FilePickerResult +import io.reactivex.Observable + +class QuickAndroidFilePicker(fragmentManager: FragmentManager) { + companion object { + const val TAG = "QuickAndroidFilePicker" + + fun create(activity: FragmentActivity): QuickAndroidFilePicker { + return QuickAndroidFilePicker(activity.supportFragmentManager) + } + + fun create(fragment: Fragment): QuickAndroidFilePicker { + return QuickAndroidFilePicker(fragment.childFragmentManager) + } + } + + private var mRxAndroidFilePickerFragment: Lazy + + init { + mRxAndroidFilePickerFragment = + fragmentManager.getLazySingleton(TAG, QuickAndroidFilePickerFragment::class.java) + } + + @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) + fun pickerFile(fileType: String = "*/*"): Observable { + return mRxAndroidFilePickerFragment + .value + .pickerFile(fileType) + } +} + +////////////////////////////////////////////////////////// +////////// QuickAndroid 扩展 +////////////////////////////////////////////////////////// + +fun QuickAndroid.createFilePicker(activity: FragmentActivity): QuickAndroidFilePicker { + return QuickAndroidFilePicker(activity.supportFragmentManager) +} + +fun QuickAndroid.createFilePicker(fragment: Fragment): QuickAndroidFilePicker { + return QuickAndroidFilePicker(fragment.childFragmentManager) +} \ No newline at end of file diff --git a/quick_android_file_picker/src/main/java/com/example/quick_android_file_picker/QuickAndroidFilePickerFragment.kt b/quick_android_file_picker/src/main/java/com/example/quick_android_file_picker/QuickAndroidFilePickerFragment.kt new file mode 100644 index 0000000..249fa12 --- /dev/null +++ b/quick_android_file_picker/src/main/java/com/example/quick_android_file_picker/QuickAndroidFilePickerFragment.kt @@ -0,0 +1,49 @@ +package com.example.quick_android_file_picker + +import android.content.Intent +import android.os.Bundle +import androidx.annotation.RequiresPermission +import androidx.fragment.app.Fragment +import cn.qjm253.quick_android_base.extensions.i +import cn.qjm253.quick_android_base.util.ContentUriUtil +import com.example.quick_android_file_picker.model.FilePickerResult +import io.reactivex.Observable +import io.reactivex.ObservableEmitter + +/** + * 这个fragment用于接收文件选择的结果 + */ +class QuickAndroidFilePickerFragment : Fragment() { + private var emitter: ObservableEmitter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + retainInstance = true + } + + @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) + fun pickerFile(fileType: String = "*/*"): Observable { + return Observable.create { _emitter -> + this.emitter = _emitter + + // 调用系统的文件选择器 + Intent(Intent.ACTION_GET_CONTENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = fileType + this@QuickAndroidFilePickerFragment.startActivityForResult(this, 0) + } + } + + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + data?.data?.let { + it.path?.i() + val path = ContentUriUtil.getPath(context, it) + println(it.toString()) + emitter?.onNext(FilePickerResult(path, it)) + emitter?.onComplete() + } + } +} \ No newline at end of file diff --git a/quick_android_file_picker/src/main/java/com/example/quick_android_file_picker/model/FilePickerResult.kt b/quick_android_file_picker/src/main/java/com/example/quick_android_file_picker/model/FilePickerResult.kt new file mode 100644 index 0000000..352227f --- /dev/null +++ b/quick_android_file_picker/src/main/java/com/example/quick_android_file_picker/model/FilePickerResult.kt @@ -0,0 +1,8 @@ +package com.example.quick_android_file_picker.model + +import android.net.Uri + +data class FilePickerResult ( + val path: String, + val uri: Uri +) \ No newline at end of file diff --git a/quick_android_file_picker/src/main/res/values/strings.xml b/quick_android_file_picker/src/main/res/values/strings.xml new file mode 100644 index 0000000..8542005 --- /dev/null +++ b/quick_android_file_picker/src/main/res/values/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/quick_android_file_picker/src/test/java/com/example/quick_android_file_picker/ExampleUnitTest.kt b/quick_android_file_picker/src/test/java/com/example/quick_android_file_picker/ExampleUnitTest.kt new file mode 100644 index 0000000..98c2794 --- /dev/null +++ b/quick_android_file_picker/src/test/java/com/example/quick_android_file_picker/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.example.quick_android_file_picker + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/quick_android_image_picker/.gitignore b/quick_android_image_picker/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/quick_android_image_picker/.gitignore @@ -0,0 +1 @@ +/build diff --git a/quick_android_image_picker/README.md b/quick_android_image_picker/README.md new file mode 100644 index 0000000..8a15efa --- /dev/null +++ b/quick_android_image_picker/README.md @@ -0,0 +1,85 @@ +> ## quick_android_image_picker 模块使用说明 +> +> 本模块基于[RxImagePicker](https://github.com/qingmei2/RxImagePicker)进行封装,并额外提供图片裁剪功能,提供简单的调用接口。关于`RxImagePicker`的更多使用说明,请参考[RxImagePicker](https://github.com/qingmei2/RxImagePicker) + +- ### 使用 + + - 简单使用 + + ```kotlin + + /** + * 打开知乎样式的图片选择器 + */ + QuickAndroidImagePicker + .openZhihuStyleGallery( + context, + ZhihuConfigurationBuilder( + MimeType.ofImage(), + false + ) + .maxSelectable(1) + .countable(true) + .spanCount(4) + .build() // ICustomPickerConfiguration 类型 + ) + .subscribe({ result -> + // 将结果传给图片裁剪界面进行图片裁剪 + QuickAndroidImagePicker.startClipImage(context, result.uri) + }) + + ``` + - 其他功能 + ```kotlin + /** + * 打开知乎样式的图片选择器 + */ + QuickAndroidImagePicker + .openWechatStyleGallery( + context, + WechatConfigrationBuilder( + MimeType.ofImage(), + false + ) + .maxSelectable(1) + .countable(true) + .spanCount(4) + .build() // ICustomPickerConfiguration 类型 + ) + .subscribe({ result -> + + }) + + /** + * 打开摄像头拍摄 + */ + QuickAndroidImagePicker + .openCamera(context) + .subscribe({ result -> + }) + + /** + * 跳转到裁剪页面裁剪图片 + * @param context + * @param uri Uri类型 + * @param multiple 裁剪后文件的压缩强度 + * 0.2 -> 50 ~ 100 KB + * 0.3 -> 100 ~ 200 KB + */ + QuickAndroidImagePicker + .startClipImage(context, uri, 0.3) + + /** + * 跳转到裁剪页面裁剪图片 + * @param context + * @param path 文件的绝对路径 + * @param multiple 裁剪后文件的压缩强度 + * 0.2 -> 50 ~ 100 KB + * 0.3 -> 100 ~ 200 KB + */ + QuickANdroidImagePicker + .startClipImage(context, path, 0.3) + + ``` + + 当然,上面的所有接口都有`QuickAndroid`对应的扩展,都可以用`QuickAndroid.xxx`来调用 diff --git a/quick_android_image_picker/build.gradle b/quick_android_image_picker/build.gradle new file mode 100644 index 0000000..75fb167 --- /dev/null +++ b/quick_android_image_picker/build.gradle @@ -0,0 +1,50 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + + +android { + compileSdkVersion compile_sdk_version + + defaultConfig { + minSdkVersion min_sdk_version + targetSdkVersion target_sdk_version + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "androidx.appcompat:appcompat:${appcompact_version}" + testImplementation "junit:junit:${junit_version}" + implementation "androidx.constraintlayout:constraintlayout:${constraintlayout_version}" + androidTestImplementation "androidx.test:runner:${test_runner_version}" + androidTestImplementation "androidx.test.espresso:espresso-core:${espresso_core_version}" + + // RxImagePicker + api "com.github.qingmei2:rximagepicker:${rx_image_picker_version}" + api "com.github.qingmei2:rximagepicker_support:${rx_image_picker_version}" + // Zhihu picture selector + api "com.github.qingmei2:rximagepicker_support_zhihu:${rx_image_picker_version}" + // WeChat picture selector + api "com.github.qingmei2:rximagepicker_support_wechat:${rx_image_picker_version}" + + + implementation project(path: ':quick_android_base') + +} +repositories { + mavenCentral() +} diff --git a/quick_android_image_picker/proguard-rules.pro b/quick_android_image_picker/proguard-rules.pro new file mode 100644 index 0000000..af8464c --- /dev/null +++ b/quick_android_image_picker/proguard-rules.pro @@ -0,0 +1,26 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + + +##------------- Begin RxImagePicker-------------## +-keep class com.qingmei2.rximagepicker.**{*;} +##------------- Begin RxImagePicker-------------## diff --git a/quick_android_image_picker/src/androidTest/java/cn/qjm253/quick_android_image_picker/ExampleInstrumentedTest.java b/quick_android_image_picker/src/androidTest/java/cn/qjm253/quick_android_image_picker/ExampleInstrumentedTest.java new file mode 100644 index 0000000..54b7655 --- /dev/null +++ b/quick_android_image_picker/src/androidTest/java/cn/qjm253/quick_android_image_picker/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package cn.qjm253.quick_android_image_picker; + +import android.content.Context; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("cn.qjm253.quick_android_image_picker.test", appContext.getPackageName()); + } +} diff --git a/quick_android_image_picker/src/main/AndroidManifest.xml b/quick_android_image_picker/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e2df19f --- /dev/null +++ b/quick_android_image_picker/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/MyImagePicker.kt b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/MyImagePicker.kt new file mode 100644 index 0000000..1295aee --- /dev/null +++ b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/MyImagePicker.kt @@ -0,0 +1,35 @@ +package cn.qjm253.quick_android_image_picker + +import android.content.Context +import com.qingmei2.rximagepicker.entity.Result +import com.qingmei2.rximagepicker.entity.sources.Camera +import com.qingmei2.rximagepicker.entity.sources.Gallery +import com.qingmei2.rximagepicker.ui.ICustomPickerConfiguration +import com.qingmei2.rximagepicker_extension_wechat.ui.WechatImagePickerActivity +import com.qingmei2.rximagepicker_extension_zhihu.ui.ZhihuImagePickerActivity +import io.reactivex.Observable + +/** + * @author SunnyQjm + * @date 19-7-7 下午1:52 + */ + +interface MyImagePicker { + + /** + * 打开类知乎的图片选择器 + */ + @Gallery(componentClazz = ZhihuImagePickerActivity::class, openAsFragment = false) + fun openZhihuStyleGallery(context: Context, + config: ICustomPickerConfiguration): Observable + + /** + * 打开类微信的图片选择器 + */ + @Gallery(componentClazz = WechatImagePickerActivity::class, openAsFragment = false) + fun openWechatStyleGallery(context: Context, + config: ICustomPickerConfiguration): Observable + + @Camera + fun openCamera(context: Context): Observable +} \ No newline at end of file diff --git a/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/QuickAndroidImagePicker.kt b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/QuickAndroidImagePicker.kt new file mode 100644 index 0000000..130bd1a --- /dev/null +++ b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/QuickAndroidImagePicker.kt @@ -0,0 +1,174 @@ +package cn.qjm253.quick_android_image_picker + +import android.content.Context +import android.net.Uri +import cn.qjm253.quick_android_base.QuickAndroid +import cn.qjm253.quick_android_base.extensions.jumpTo +import cn.qjm253.quick_android_base.params.IntentParam +import cn.qjm253.quick_android_base.util.ContentUriUtil +import cn.qjm253.quick_android_image_picker.activity.QuickAndroidClipImageActivity +import com.qingmei2.rximagepicker.core.RxImagePicker +import com.qingmei2.rximagepicker.entity.Result +import com.qingmei2.rximagepicker.ui.ICustomPickerConfiguration +import com.qingmei2.rximagepicker_extension.MimeType +import com.qingmei2.rximagepicker_extension_wechat.WechatConfigrationBuilder +import com.qingmei2.rximagepicker_extension_zhihu.ZhihuConfigurationBuilder +import io.reactivex.Observable + +/** + * @author SunnyQjm + * @date 19-7-7 下午2:25 + */ +object QuickAndroidImagePicker { + + /** + * 打开知乎样式的图片选择器 + */ + fun openZhihuStyleGallery( + context: Context, + config: ICustomPickerConfiguration = ZhihuConfigurationBuilder(MimeType.ofImage(), false) + .maxSelectable(9) + .countable(true) + .spanCount(4) + .theme(R.style.Zhihu_Normal) + .build() + ): Observable { + return RxImagePicker + .create(MyImagePicker::class.java) + .openZhihuStyleGallery(context, config) + } + + /** + * 打开微信样式的图片选择器 + */ + fun openWechatStyleGallery( + context: Context, + config: ICustomPickerConfiguration = WechatConfigrationBuilder(MimeType.ofImage(), false) + .maxSelectable(9) + .countable(true) + .spanCount(4) + .build() + ): Observable { + return RxImagePicker + .create(MyImagePicker::class.java) + .openWechatStyleGallery(context, config) + } + + + /** + * 打开摄像头拍摄 + */ + fun openCamera(context: Context): Observable { + return RxImagePicker + .create() + .openCamera(context) + } + + /** + * 跳转到裁剪页面裁剪图片 + * @param context + * @param path 文件的绝对路径 + * @param multiple 裁剪后文件的压缩强度 + * 0.2 -> 50 ~ 100 KB + * 0.3 -> 100 ~ 200 KB + */ + fun startClipImage(context: Context, path: String, multiple: Float = 0.3f): QuickAndroidImagePicker { + context.jumpTo( + QuickAndroidClipImageActivity::class.java, IntentParam() + .add(QuickAndroidClipImageActivity.QUICK_ANDROID_CLIP_IMAGE_IMAGE_PATH, path) + .add(QuickAndroidClipImageActivity.QUICK_ANDROID_CLIP_IMAGE_MULTIPLE, multiple) + ) + return this + } + + /** + * 跳转到裁剪页面裁剪图片 + * @param context + * @param uri Uri类型 + * @param multiple 裁剪后文件的压缩强度 + * 0.2 -> 50 ~ 100 KB + * 0.3 -> 100 ~ 200 KB + */ + fun startClipImage(context: Context, uri: Uri, multiple: Float = 0.3f): QuickAndroidImagePicker { + startClipImage(context, ContentUriUtil.getPath(context, uri), multiple) + return this + } + +} + +////////////////////////////////////////////////////////// +////////// QuickAndroid 扩展 +////////////////////////////////////////////////////////// + +fun QuickAndroid.openZhihuStyleGallery( + context: Context, + config: ICustomPickerConfiguration = ZhihuConfigurationBuilder(MimeType.ofImage(), false) + .maxSelectable(9) + .countable(true) + .spanCount(4) + .theme(R.style.Zhihu_Normal) + .build() +): Observable { + return QuickAndroidImagePicker.openZhihuStyleGallery(context, config) +} + +fun QuickAndroid.openWechatStyleGallery( + context: Context, + config: ICustomPickerConfiguration = WechatConfigrationBuilder(MimeType.ofImage(), false) + .maxSelectable(9) + .countable(true) + .spanCount(4) + .build() +): Observable { + return QuickAndroidImagePicker.openWechatStyleGallery(context, config) +} + +fun QuickAndroid.openCamera(context: Context): Observable { + return QuickAndroidImagePicker.openCamera(context) +} + +fun QuickAndroid.startClipImage(context: Context, path: String, multiple: Float = 0.3f): QuickAndroidImagePicker { + return QuickAndroidImagePicker.startClipImage(context, path, multiple) +} + +fun QuickAndroid.startClipImage(context: Context, uri: Uri, multiple: Float = 0.3f): QuickAndroidImagePicker { + return QuickAndroidImagePicker.startClipImage(context, uri, multiple) +} + + +////////////////////////////////////////////////////////// +////////// Context 扩展 +////////////////////////////////////////////////////////// + +fun Context.openZhihuStyleGallery( + config: ICustomPickerConfiguration = ZhihuConfigurationBuilder(MimeType.ofImage(), false) + .maxSelectable(9) + .countable(true) + .spanCount(4) + .theme(R.style.Zhihu_Normal) + .build() +): Observable { + return QuickAndroidImagePicker.openZhihuStyleGallery(this, config) +} + +fun Context.openWechatStyleGallery( + config: ICustomPickerConfiguration = WechatConfigrationBuilder(MimeType.ofImage(), false) + .maxSelectable(9) + .countable(true) + .spanCount(4) + .build() +): Observable { + return QuickAndroidImagePicker.openWechatStyleGallery(this, config) +} + +fun Context.openCamera(): Observable { + return QuickAndroidImagePicker.openCamera(this) +} + +fun Context.startClipImage(path: String, multiple: Float = 0.3f): QuickAndroidImagePicker { + return QuickAndroidImagePicker.startClipImage(this, path, multiple) +} + +fun Context.startClipImage(uri: Uri, multiple: Float = 0.3f): QuickAndroidImagePicker { + return QuickAndroidImagePicker.startClipImage(this, uri, multiple) +} \ No newline at end of file diff --git a/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/activity/QuickAndroidClipImageActivity.kt b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/activity/QuickAndroidClipImageActivity.kt new file mode 100644 index 0000000..fde883e --- /dev/null +++ b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/activity/QuickAndroidClipImageActivity.kt @@ -0,0 +1,42 @@ +package cn.qjm253.quick_android_image_picker.activity + +import android.os.Bundle +import cn.qjm253.quick_android_base.base.activity.BaseQuickAndroidActivity +import cn.qjm253.quick_android_base.base.fragment.BaseQuickAndroidFragment +import cn.qjm253.quick_android_base.extensions.addFragmentToActivity +import cn.qjm253.quick_android_image_picker.R +import cn.qjm253.quick_android_image_picker.fragment.QuickAndroidClipImageFragment +import com.gyf.immersionbar.BarHide +import com.gyf.immersionbar.ImmersionBar +import kotlinx.android.synthetic.main.activity_quick_android_clip_image.* + +class QuickAndroidClipImageActivity : BaseQuickAndroidActivity(), BaseQuickAndroidFragment.OnFragmentInteractionListener { + + companion object { + const val QUICK_ANDROID_CLIP_IMAGE_IMAGE_PATH = QuickAndroidClipImageFragment.QUICK_ANDROID_CLIP_IMAGE_IMAGE_PATH + const val QUICK_ANDROID_CLIP_IMAGE_MULTIPLE = QuickAndroidClipImageFragment.QUICK_ANDROID_CLIP_IMAGE_MULTIPLE + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ImmersionBar.with(this) + .fullScreen(true) + .hideBar(BarHide.FLAG_HIDE_NAVIGATION_BAR) + .init() + setContentView(R.layout.activity_quick_android_clip_image) + + // 如果有的话隐藏ActionBar + supportActionBar?.hide() + + val path = intent.getStringExtra(QUICK_ANDROID_CLIP_IMAGE_IMAGE_PATH) + val multiple = intent.getFloatExtra(QUICK_ANDROID_CLIP_IMAGE_MULTIPLE, QuickAndroidClipImageFragment.DEFAULT_MULTIPLE) + + val fragment = addFragmentToActivity(R.id.quickAndroidClipImageFragmentContainer) { + return@addFragmentToActivity QuickAndroidClipImageFragment.newInstance(path, multiple) + } + + tvFinish.setOnClickListener { + fragment.doClip() + } + } +} diff --git a/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/fragment/QuickAndroidClipImageFragment.kt b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/fragment/QuickAndroidClipImageFragment.kt new file mode 100644 index 0000000..e076da3 --- /dev/null +++ b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/fragment/QuickAndroidClipImageFragment.kt @@ -0,0 +1,118 @@ +package cn.qjm253.quick_android_image_picker.fragment + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.graphics.Bitmap +import cn.qjm253.quick_android_base.base.fragment.BaseQuickAndroidFragment +import cn.qjm253.quick_android_base.extensions.e +import cn.qjm253.quick_android_base.extensions.i +import cn.qjm253.quick_android_base.params.IntentParam +import cn.qjm253.quick_android_base.util.FileUtils +import cn.qjm253.quick_android_base.util.RxSchedulersHelper +import cn.qjm253.quick_android_image_picker.R +import io.reactivex.Observable +import io.reactivex.ObservableEmitter +import kotlinx.android.synthetic.main.fragment_quick_android_clip_image.* +import java.io.File +import java.lang.Exception + +/** + * 图片裁剪Fragment + * + * @author SunnyQjm + * @date 19-7-7 下午3:58 + */ +class QuickAndroidClipImageFragment : BaseQuickAndroidFragment() { + + companion object { + // 需要裁剪的原图片路径 + const val QUICK_ANDROID_CLIP_IMAGE_IMAGE_PATH = "QUICK_ANDROID_CLIP_IMAGE_IMAGE_PATH" + + // 压缩强度 + const val QUICK_ANDROID_CLIP_IMAGE_MULTIPLE = "QUICK_ANDROID_CLIP_IMAGE_MULTIPLE" + + // 裁剪后的图片路径 + const val QUICK_ANDROID_CLIP_IMAGE_RESULT = "QUICK_ANDROID_CLIP_IMAGE_RESULT" + + const val QUICK_ANDROID_CLIP_IMAGE_RW_PERMISSION_REQUEST_CODE = 1 + + const val DEFAULT_MULTIPLE = 0.3f + fun newInstance(path: String, multiple: Float = 0.3f): QuickAndroidClipImageFragment { + return newInstance_( + QuickAndroidClipImageFragment::class.java, IntentParam() + .add(QUICK_ANDROID_CLIP_IMAGE_IMAGE_PATH, path) + .add(QUICK_ANDROID_CLIP_IMAGE_MULTIPLE, multiple) + ) + } + } + + + private var multiple: Float = DEFAULT_MULTIPLE + + override fun getRes(): Int = R.layout.fragment_quick_android_clip_image + + override fun initView() { + val imagePath = arguments?.getString(QUICK_ANDROID_CLIP_IMAGE_IMAGE_PATH) ?: "" + multiple = arguments?.getFloat(QUICK_ANDROID_CLIP_IMAGE_MULTIPLE, DEFAULT_MULTIPLE) ?: DEFAULT_MULTIPLE + + // 检查边界,限制在 0~1之间 + if (multiple < 0) { + multiple = 0.1f + } else if (multiple > 1f) { + multiple = 1f + } + clipImageLayout.setImagePath(imagePath) + } + + @SuppressLint("CheckResult") + private fun doCLip_() { + Observable.create { emitter: ObservableEmitter -> + try { + FileUtils.generatePictureFile("clip.png").apply { + if (!exists()) + createNewFile() + outputStream().use { + clipImageLayout.clip(multiple) + .compress(Bitmap.CompressFormat.PNG, 100, it) + } + emitter.onNext(this) + emitter.onComplete() + } + } catch (err: Exception) { + emitter.onError(err) + } + } + .compose(RxSchedulersHelper.io_main()) + .subscribe({ file -> + Intent().let { + it.putExtra(QUICK_ANDROID_CLIP_IMAGE_RESULT, file.absolutePath) + activity?.setResult(0, it) + activity?.finish() + } + "${file.absolutePath} 压缩成功 (${file.length()})".i() + }, { + it.e() + }) + } + + fun doClip() { + requestPermissions( + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), + R.string.clip_image_permission_tip, QUICK_ANDROID_CLIP_IMAGE_RW_PERMISSION_REQUEST_CODE + ) + } + + override fun onPermissionsGranted(requestCode: Int, perms: MutableList) { + super.onPermissionsGranted(requestCode, perms) + when (requestCode) { + QUICK_ANDROID_CLIP_IMAGE_RW_PERMISSION_REQUEST_CODE -> { + doCLip_() + } + } + } + + override fun initialLoadData() { + } + +} \ No newline at end of file diff --git a/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/views/ClipImageBorderView.kt b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/views/ClipImageBorderView.kt new file mode 100644 index 0000000..a20d011 --- /dev/null +++ b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/views/ClipImageBorderView.kt @@ -0,0 +1,107 @@ +package cn.qjm253.quick_android_image_picker.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Paint.Style +import android.util.AttributeSet +import android.util.TypedValue +import android.view.View + +/** + * @author fucker + */ +class ClipImageBorderView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : + View(context, attrs, defStyle) { + /** + * 水平方向与View的边距 + */ + private var mHorizontalPadding: Int = 0 + /** + * 垂直方向与View的边距 + */ + private var mVerticalPadding: Int = 0 + /** + * 裁剪框的纵横比,mVerticalPadding通过该值计算 + * 默认是正方形,所以等于1 + */ + private var aspectRatio = 1f + /** + * 绘制的矩形的宽度 + */ + private var mWidth: Int = 0 + /** + * 边框的颜色,默认为白色 + */ + private val mBorderColor = Color.parseColor("#FFFFFF") + /** + * 边框的宽度单位dp + */ + private var mBorderWidth = 1 + + private val mPaint: Paint + + init { + + mBorderWidth = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, mBorderWidth.toFloat(), resources + .displayMetrics + ).toInt() + mPaint = Paint() + mPaint.isAntiAlias = true + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + // 计算矩形区域的宽度 + mWidth = width - 2 * mHorizontalPadding + // 计算距离屏幕垂直边界的边距 + mVerticalPadding = ((height - mWidth * aspectRatio) / 2).toInt() + mPaint.color = Color.parseColor("#000000") + mPaint.alpha = (255 * 0.48f).toInt() + mPaint.style = Style.FILL + // 绘制左边1 + canvas.drawRect(0f, 0f, mHorizontalPadding.toFloat(), height.toFloat(), mPaint) + // 绘制右边2 + canvas.drawRect( + (width - mHorizontalPadding).toFloat(), 0f, width.toFloat(), + height.toFloat(), mPaint + ) + // 绘制上边3 + canvas.drawRect( + mHorizontalPadding.toFloat(), 0f, (width - mHorizontalPadding).toFloat(), + mVerticalPadding.toFloat(), mPaint + ) + // 绘制下边4 + canvas.drawRect( + mHorizontalPadding.toFloat(), (height - mVerticalPadding).toFloat(), + (width - mHorizontalPadding).toFloat(), height.toFloat(), mPaint + ) + // 绘制外边距 + mPaint.color = mBorderColor + mPaint.strokeWidth = mBorderWidth.toFloat() + mPaint.style = Style.STROKE + canvas.drawRect( + mHorizontalPadding.toFloat(), + mVerticalPadding.toFloat(), + (width - mHorizontalPadding).toFloat(), + (height - mVerticalPadding).toFloat(), + mPaint + ) + + } + + fun setHorizontalPadding(mHorizontalPadding: Int) { + this.mHorizontalPadding = mHorizontalPadding + } + + /** + * 设置clip框的纵横比 + * + * @param aspectRatio + */ + fun setAspectRatio(aspectRatio: Float) { + this.aspectRatio = aspectRatio + } +} \ No newline at end of file diff --git a/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/views/ClipImageLayout.kt b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/views/ClipImageLayout.kt new file mode 100644 index 0000000..a1e92f4 --- /dev/null +++ b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/views/ClipImageLayout.kt @@ -0,0 +1,104 @@ +package cn.qjm253.quick_android_image_picker.views + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.AttributeSet +import android.util.TypedValue +import android.widget.RelativeLayout +import cn.qjm253.quick_android_base.util.DisplayUtils +import kotlin.math.roundToInt + + +/** + * @author fucker + */ +class ClipImageLayout(context: Context, attrs: AttributeSet) : RelativeLayout(context, attrs) { + + private val mZoomImageView: ClipZoomImageView = ClipZoomImageView(context) + private val mClipImageView: ClipImageBorderView = ClipImageBorderView(context) + + private var mHorizontalPadding = 24 + private var aspectRatio = 1f + + init { + + val lp = LayoutParams( + android.view.ViewGroup.LayoutParams.MATCH_PARENT, + android.view.ViewGroup.LayoutParams.MATCH_PARENT + ) + + this.addView(mZoomImageView, lp) + this.addView(mClipImageView, lp) + + setHorizontalPadding(mHorizontalPadding) + setAspectRatio(aspectRatio) + } + + fun setImageBitmap(bm: Bitmap) { + mZoomImageView.setImageBitmap(bm) + } + + fun setImagePath(path: String) { + val screen = DisplayUtils.getScreenParams(context) + val width = screen[0] + val height = screen[1] + + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(path, options) + var scale = (if (options.outWidth * 1.0 / width < 1) + 1f + else + options.outWidth * 1.0f / width) + val scale2 = (if (options.outHeight * 1.0 / height < 1) + 1f + else + options.outHeight * 1.0f / height) + scale = if (scale < scale2) scale2 else scale//select a bigger scale number. + options.outHeight = (options.outHeight / scale).toInt() + options.outWidth = (options.outWidth / scale).toInt() + options.inSampleSize = scale.roundToInt() + options.inJustDecodeBounds = false + val bm = BitmapFactory.decodeFile(path, options) + setImageBitmap(bm) + } + + /** + * 设置左右边距的方法 + * + * @param mHorizontalPadding dp + */ + fun setHorizontalPadding(mHorizontalPadding: Int) { + var mHorizontalPadding = mHorizontalPadding + this.mHorizontalPadding = mHorizontalPadding + // 计算padding的px + mHorizontalPadding = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, mHorizontalPadding.toFloat(), resources + .displayMetrics + ).toInt() + mZoomImageView.setHorizontalPadding(mHorizontalPadding) + mClipImageView.setHorizontalPadding(mHorizontalPadding) + } + + /** + * 设置裁剪图像的纵横比 + * + * @param aspectRatio = height / width + */ + fun setAspectRatio(aspectRatio: Float) { + this.aspectRatio = aspectRatio + mZoomImageView.setAspectRatio(aspectRatio) + mClipImageView.setAspectRatio(aspectRatio) + } + + /** + * 裁切图片,multiple 0~1.0f + * + * @return + */ + fun clip(multiple: Float): Bitmap { + return mZoomImageView.clip(multiple) + } + +} \ No newline at end of file diff --git a/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/views/ClipZoomImageView.kt b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/views/ClipZoomImageView.kt new file mode 100644 index 0000000..563b724 --- /dev/null +++ b/quick_android_image_picker/src/main/java/cn/qjm253/quick_android_image_picker/views/ClipZoomImageView.kt @@ -0,0 +1,361 @@ +package cn.qjm253.quick_android_image_picker.views + + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.RectF +import android.util.AttributeSet +import android.view.* +import androidx.appcompat.widget.AppCompatImageView +import kotlin.math.sqrt + +/** + * @author fucker + */ +@SuppressLint("ClickableViewAccessibility") +class ClipZoomImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + AppCompatImageView(context, attrs), ScaleGestureDetector.OnScaleGestureListener, View.OnTouchListener, + ViewTreeObserver.OnGlobalLayoutListener { + + private var initScale = 0.5f + private var once = true + + private val matrixValues = FloatArray(9) + + private var mScaleGestureDetector: ScaleGestureDetector? = null + private val mScaleMatrix = Matrix() + + /** + */ + private val mGestureDetector: GestureDetector + private var isAutoScale: Boolean = false + + private val mTouchSlop: Int = 0 + + private var mLastX: Float = 0.toFloat() + private var mLastY: Float = 0.toFloat() + + private var isCanDrag: Boolean = false + private var lastPointerCount: Int = 0 + + private val matrixRectF: RectF + get() { + val matrix = mScaleMatrix + val rect = RectF() + val d = drawable + if (null != d) { + rect.set(0f, 0f, d.intrinsicWidth.toFloat(), d.intrinsicHeight.toFloat()) + matrix.mapRect(rect) + } + return rect + } + + val scale: Float + get() { + mScaleMatrix.getValues(matrixValues) + return matrixValues[Matrix.MSCALE_X] + } + + private var mHorizontalPadding: Int = 0 + private var mVerticalPadding: Int = 0 + /** + * 裁剪框的纵横比,mVerticalPadding通过该值计算 + * 默认是正方形,所以等于1 + */ + private var aspectRatio = 1f + + init { + + scaleType = ScaleType.MATRIX + mGestureDetector = GestureDetector(context, + object : GestureDetector.SimpleOnGestureListener() { + override fun onDoubleTap(e: MotionEvent): Boolean { + if (isAutoScale == true) + return true + + val x = e.x + val y = e.y + if (scale < SCALE_MID) { + this@ClipZoomImageView.postDelayed( + AutoScaleRunnable(SCALE_MID, x, y), 16 + ) + isAutoScale = true + } else { + this@ClipZoomImageView.postDelayed( + AutoScaleRunnable(initScale, x, y), 16 + ) + isAutoScale = true + } + + return true + } + }) + mScaleGestureDetector = ScaleGestureDetector(context, this) + this.setOnTouchListener(this) + } + + fun setInitScale(scale: Float) { + if (drawable != null) { + this.initScale = scale + } + } + + /** + * @author zhy + */ + private inner class AutoScaleRunnable + /** + * @param targetScale + */ + (private val mTargetScale: Float, private val x: Float, private val y: Float) : Runnable { + private var tmpScale: Float = 0.toFloat() + + init { + if (scale < mTargetScale) { + tmpScale = 1.07f + } else { + tmpScale = 0.93f + } + + } + + override fun run() { + mScaleMatrix.postScale(tmpScale, tmpScale, x, y) + checkBorder() + imageMatrix = mScaleMatrix + + val currentScale = scale + if (tmpScale > 1f && currentScale < mTargetScale || tmpScale < 1f && mTargetScale < currentScale) { + this@ClipZoomImageView.postDelayed(this, 16) + } else { + val deltaScale = mTargetScale / currentScale + mScaleMatrix.postScale(deltaScale, deltaScale, x, y) + checkBorder() + imageMatrix = mScaleMatrix + isAutoScale = false + } + + } + + } + + override fun onScale(detector: ScaleGestureDetector): Boolean { + val scale = scale + var scaleFactor = detector.scaleFactor + + if (drawable == null) + return true + + if (scale < SCALE_MAX && scaleFactor > 1.0f || scale > initScale && scaleFactor <= 1.0f) { + if (scaleFactor * scale < initScale) { + scaleFactor = initScale / scale + } + if (scaleFactor * scale > SCALE_MAX) { + scaleFactor = SCALE_MAX / scale + } + mScaleMatrix.postScale( + scaleFactor, scaleFactor, + detector.focusX, detector.focusY + ) + checkBorder() + imageMatrix = mScaleMatrix + } + return true + + } + + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + return true + } + + @SuppressLint("ClickableViewAccessibility") + override fun onScaleEnd(detector: ScaleGestureDetector) { + } + + override fun onTouch(v: View, event: MotionEvent): Boolean { + + if (mGestureDetector.onTouchEvent(event)) + return true + mScaleGestureDetector!!.onTouchEvent(event) + + var x = 0f + var y = 0f + val pointerCount = event.pointerCount + for (i in 0 until pointerCount) { + x += event.getX(i) + y += event.getY(i) + } + x /= pointerCount + y /= pointerCount + + if (pointerCount != lastPointerCount) { + isCanDrag = false + mLastX = x + mLastY = y + } + + lastPointerCount = pointerCount + when (event.action) { + MotionEvent.ACTION_MOVE -> { + var dx = x - mLastX + var dy = y - mLastY + + if (!isCanDrag) { + isCanDrag = isCanDrag(dx, dy) + } + if (isCanDrag) { + if (drawable != null) { + + val rectF = matrixRectF + + if (rectF.width() <= width - mHorizontalPadding * 2) { + dx = 0f + } + if (rectF.height() <= height - mVerticalPadding * 2) { + dy = 0f + } + mScaleMatrix.postTranslate(dx, dy) + checkBorder() + imageMatrix = mScaleMatrix + } + } + mLastX = x + mLastY = y + } + + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> lastPointerCount = 0 + } + + return true + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + viewTreeObserver.addOnGlobalLayoutListener(this) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + viewTreeObserver.removeGlobalOnLayoutListener(this) + } + + override fun onGlobalLayout() { + if (once) { + val d = drawable ?: return + mVerticalPadding = ((height - aspectRatio * (width - 2 * mHorizontalPadding)) / 2).toInt() + + val width = width + val height = height + + val dw = d.intrinsicWidth + val dh = d.intrinsicHeight + var scale = 1.0f + if (dw < getWidth() - mHorizontalPadding * 2 && dh > getHeight() - mVerticalPadding * 2) { + scale = (getWidth() * 1.0f - mHorizontalPadding * 2) / dw + } + + if (dh < getHeight() - mVerticalPadding * 2 && dw > getWidth() - mHorizontalPadding * 2) { + scale = (getHeight() * 1.0f - mVerticalPadding * 2) / dh + } + + if (dw < getWidth() - mHorizontalPadding * 2 && dh < getHeight() - mVerticalPadding * 2) { + val scaleW = (getWidth() * 1.0f - mHorizontalPadding * 2) / dw + val scaleH = (getHeight() * 1.0f - mVerticalPadding * 2) / dh + scale = Math.max(scaleW, scaleH) + } + if (dw >= getWidth() - mHorizontalPadding * 2 && dh >= getHeight() - mVerticalPadding * 2) { + val scaleW = (getWidth() * 1.0f - mHorizontalPadding * 2) / dw + val scaleH = (getHeight() * 1.0f - mVerticalPadding * 2) / dh + scale = Math.max(scaleW, scaleH) + } + + initScale = scale + SCALE_MID = initScale * 2 + SCALE_MAX = initScale * 4 + mScaleMatrix.postTranslate(((width - dw) / 2).toFloat(), ((height - dh) / 2).toFloat()) + mScaleMatrix.postScale( + scale, scale, (getWidth() / 2).toFloat(), + (getHeight() / 2).toFloat() + ) + + imageMatrix = mScaleMatrix + once = false + } + + } + + fun clip(multiple: Float): Bitmap { + var multiple = multiple + multiple = if (multiple <= 0) 1.0f else multiple + multiple = if (multiple > 1) 1.0f else multiple + val width = width + val height = height + val bitmap = Bitmap.createBitmap( + width, height, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + draw(canvas) + val matrix = Matrix() + matrix.postScale(multiple, multiple) + return Bitmap.createBitmap( + bitmap, mHorizontalPadding, + mVerticalPadding, width - 2 * mHorizontalPadding, height - 2 * mVerticalPadding, matrix, true + ) + } + + private fun checkBorder() { + + val rect = matrixRectF + var deltaX = 0f + var deltaY = 0f + + val width = width + val height = height + if (rect.width() + 0.01 >= width - 2 * mHorizontalPadding) { + if (rect.left > mHorizontalPadding) { + deltaX = -rect.left + mHorizontalPadding + } + if (rect.right < width - mHorizontalPadding) { + deltaX = width.toFloat() - mHorizontalPadding.toFloat() - rect.right + } + } + if (rect.height() + 0.01 >= height - 2 * mVerticalPadding) { + if (rect.top > mVerticalPadding) { + deltaY = -rect.top + mVerticalPadding + } + if (rect.bottom < height - mVerticalPadding) { + deltaY = height.toFloat() - mVerticalPadding.toFloat() - rect.bottom + } + } + mScaleMatrix.postTranslate(deltaX, deltaY) + + } + + private fun isCanDrag(dx: Float, dy: Float): Boolean { + return sqrt((dx * dx + dy * dy).toDouble()) >= mTouchSlop + } + + fun setHorizontalPadding(mHorizontalPadding: Int) { + this.mHorizontalPadding = mHorizontalPadding + } + + /** + * 设置clip框的纵横比 + * + * @param aspectRatio + */ + fun setAspectRatio(aspectRatio: Float) { + this.aspectRatio = aspectRatio + } + + companion object { + + var SCALE_MAX = 4.0f + private var SCALE_MID = 2.0f + } + +} \ No newline at end of file diff --git a/quick_android_image_picker/src/main/res/layout/activity_quick_android_clip_image.xml b/quick_android_image_picker/src/main/res/layout/activity_quick_android_clip_image.xml new file mode 100644 index 0000000..7f8f6b6 --- /dev/null +++ b/quick_android_image_picker/src/main/res/layout/activity_quick_android_clip_image.xml @@ -0,0 +1,29 @@ + + + + + + + + + \ No newline at end of file diff --git a/quick_android_image_picker/src/main/res/layout/fragment_quick_android_clip_image.xml b/quick_android_image_picker/src/main/res/layout/fragment_quick_android_clip_image.xml new file mode 100644 index 0000000..3fede61 --- /dev/null +++ b/quick_android_image_picker/src/main/res/layout/fragment_quick_android_clip_image.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/quick_android_image_picker/src/main/res/values/strings.xml b/quick_android_image_picker/src/main/res/values/strings.xml new file mode 100644 index 0000000..1a2ada8 --- /dev/null +++ b/quick_android_image_picker/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + 裁剪图片需要获取读写文件的权限 + diff --git a/quick_android_image_picker/src/test/java/cn/qjm253/quick_android_image_picker/ExampleUnitTest.java b/quick_android_image_picker/src/test/java/cn/qjm253/quick_android_image_picker/ExampleUnitTest.java new file mode 100644 index 0000000..aea8878 --- /dev/null +++ b/quick_android_image_picker/src/test/java/cn/qjm253/quick_android_image_picker/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package cn.qjm253.quick_android_image_picker; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/quick_android_mvp/.gitignore b/quick_android_mvp/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/quick_android_mvp/.gitignore @@ -0,0 +1 @@ +/build diff --git a/quick_android_mvp/README.md b/quick_android_mvp/README.md new file mode 100644 index 0000000..1b700d8 --- /dev/null +++ b/quick_android_mvp/README.md @@ -0,0 +1,172 @@ +> ## quick_android_mvp模块 +> +> 本模块提供了一个快速实现MVP架构的框架,并且封装了网络请求(Retrofit + RxJava) + +- ### 说明 + + 本模块提供提供了一系列MVP模式实现代码的封装,并且使用[retrofit](https://github.com/square/retrofit) + [RxJava2](https://github.com/ReactiveX/RxJava) + 实现了网络请求。默认使用[AutoDispose](https://github.com/uber/AutoDispose)来实现订阅对象随Activity的销毁而自动取消,防止内存泄漏 + +- ### 使用 + + - 初始化 + + ```kotlin + /** + * QuickAndroidMVP 模块初始化操作 + * + * @param context Application 对象 + * @param baseUrl 网络请求的前缀, eg.: https://example.cn/ + * @param enablePersistentCookieJar 是否开启Cookie的自动持久化和发送 + */ + QuickAndroidMVP + .init( + context, baseUrl, enablePersistentCookieJar + ) + + // 当然,如果使用Kotlin开发也可以使用下面的统一扩展接口,参数说明同上 + QuickAndroid.initMVP( + context, baseUrl, enablePersistentCookieJar + ) + ``` + + 具体MVP架构的使用方式参考本项目的[demo](https://github.com/SunnyQjm/quickandroid/tree/master/app/src/main/java/cn/qjm253/quick_android/mvp_demo) + +- ### 如何使用网络请求? + + - 首先定义网络请求的Service接口(具体定义规则参看[Retrofit 官网](https://square.github.io/retrofit/)) + + ```kotlin + interface TestServices { + + @GET("api") + fun getWeather( + @Query("city") city: String = "成都", + @Query("version") version: String = "v1" + ): Observable + } + ``` + + - 使用 + + ```kotlin + QuickAndroidMVPAPIManager.getService(TestServices::class.java) + .getWeather("成都") + .compose(RxScheduler.io_main()) + .subscribe({ // onNext + + }, { // onError + + }, { // onComplete + + }) + + ``` + + 轻松两步便可实现网络请求 + +- ### 如何对每一个网络请求添加监听? + + 下面的所有接口都有对应 `QuickAndroid` 的扩展,如果使用`kotlin`开发的话,也可以使用`QuickAndroid.xxx`的方式来调用。**所有的网络请求,只有使用`qaSubscribe`扩展进行订阅的才会调用下面的回调** + + - 添加一个可以监听所有网络请求的onComplete事件的监听器 + + ```kotlin + /** + * 添加一个网络请求 onComplete 的拦截监听 + * 每一个使用 Observable.qaSubscribe 进行订阅的网络请求的onComplete执行的时候都会触发 + * + * @see cn.qjm253.quick_android_mvp.model.inernet.rx.qaSubscribe + */ + QuickAndroidMVP.addOnAPICompleteListener(onComplete) + + // 可以通过下面的方法清空所有对onComplete事件的监听器 + QuickAndroidMVP.clearOnAPICompleteListeners() + ``` + + - 添加一个可以监听所有网络请求的onNext事件的监听器 + + ```kotlin + /** + * 添加一个网络请求 onNext 的拦截监听 + * 每一个使用 Observable.qaSubscribe 进行订阅的网络请求的onNext执行的之前都会触发 + * + * @see cn.qjm253.quick_android_mvp.model.inernet.rx.qaSubscribe + */ + QuickAndroidMVP.addOnAPICompleteListener(onBeforeNextListener) + + QuickAndroidMVP.clearOnAPIBeforeNextListeners() + ``` + + - 添加一个可以监听所有网络请求的onError事件的监听器 + + ```kotlin + /** + * 添加一个网络请求 onError 的拦截监听 + * 每一个使用 Observable.qaSubscribe 进行订阅的网络请求的onError执行的时候都会触发 + * + * @see cn.qjm253.quick_android_mvp.model.inernet.rx.qaSubscribe + */ + QuickAndroidMVP.addOnAPIErrorListener(onErrorListener) + + // 可以通过下面的方法清空所有对onError事件的监听器 + QuickAndroidMVP.clearOnAPIErrorListeners() + ``` + +- ### 如何对网络请求的结果进行统一的处理 + + - 使用要求 + + - 所有的网络请求都必须使用`qaHandleResult()`扩展 + + ```kotlin + Observable.just(BaseQuickAndroidResponseBody(-2)) + .qaHandleResult() + .qaSubscribe(mView, { + + }) + ``` + + - 网络请求的Model数据类需要继承自[BaseQuickAndroidResponseBody](https://github.com/SunnyQjm/quickandroid/blob/master/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/response/BaseQuickAndroidResponseBody.kt),里面包含一个`code`属性标识网络请求的状态码 + + - 使用 + + 网络请求成功的情况下,通常返回的结果中会包含状态码(状态码一般又分为请求正常、请求错误等等),如果需要对网络请求的结果进行统一处理,便可以使用如下的方式 + + ```kotlin + QuickAndroidMVP + .registerAPIResultDeal(object : QuickAndroidMVPResultDeal( + // 包含所有网络请求失败的code + arrayOf( + -1 to "网络请求失败" + ), + + // 包含所有请求成功的code + arrayOf( + 0 to "网络请求成功" + ), + + // token 失效,需要重新获取token之后重新发起请求的code + arrayOf( + -2 to "Token失效" + ) + ) { + override fun dealRetry(t: Throwable): Observable { + t.e("dealRetry") + return Observable.error(t) + } + + override fun normalDeal(code: Int, description: String) { + "normalDeal => code: $code, description: $description".i() + } + + override fun errorDeal(code: Int, description: String) { + "errorDeal => code: $code, description: $description".e() + } + + }) + ``` + + + + \ No newline at end of file diff --git a/quick_android_mvp/build.gradle b/quick_android_mvp/build.gradle new file mode 100644 index 0000000..4dcc7d8 --- /dev/null +++ b/quick_android_mvp/build.gradle @@ -0,0 +1,59 @@ +apply plugin: 'com.android.library' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +apply plugin: 'kotlin-kapt' + +def AUTO_DISPOSE_VERSION = "1.3.0" + +android { + compileSdkVersion compile_sdk_version + + defaultConfig { + minSdkVersion min_sdk_version + targetSdkVersion target_sdk_version + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation "androidx.appcompat:appcompat:${appcompact_version}" + testImplementation "junit:junit:${junit_version}" + androidTestImplementation "androidx.test:runner:${test_runner_version}" + androidTestImplementation "androidx.test.espresso:espresso-core:${espresso_core_version}" + + // AutoDispose + implementation "com.uber.autodispose:autodispose:${AUTO_DISPOSE_VERSION}" + implementation "com.uber.autodispose:autodispose-android:${AUTO_DISPOSE_VERSION}" + implementation "com.uber.autodispose:autodispose-lifecycle:${AUTO_DISPOSE_VERSION}" + implementation "com.uber.autodispose:autodispose-android-archcomponents:${AUTO_DISPOSE_VERSION}" + + + + // PersistentCookieJar + implementation "com.github.franmontiel:PersistentCookieJar:${persistent_cookie_jar_version}" + + + implementation project(path: ':quick_android_base') +} diff --git a/quick_android_mvp/proguard-rules.pro b/quick_android_mvp/proguard-rules.pro new file mode 100644 index 0000000..1eb290a --- /dev/null +++ b/quick_android_mvp/proguard-rules.pro @@ -0,0 +1,34 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +##---------------Begin: AndroidX---------------## +-keep class com.google.android.material.** {*;} +-keep class androidx.** {*;} +-keep public class * extends androidx.** +-keep interface androidx.** {*;} +-dontwarn com.google.android.material.** +-dontnote com.google.android.material.** +-dontwarn androidx.** +##---------------End: AndroidX---------------## + + + diff --git a/quick_android_mvp/src/androidTest/java/cn/qjm253/quick_android_mvp/ExampleInstrumentedTest.java b/quick_android_mvp/src/androidTest/java/cn/qjm253/quick_android_mvp/ExampleInstrumentedTest.java new file mode 100644 index 0000000..904e2dd --- /dev/null +++ b/quick_android_mvp/src/androidTest/java/cn/qjm253/quick_android_mvp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package cn.qjm253.quick_android_mvp; + +import android.content.Context; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("cn.qjm253.quick_android_mvp.test", appContext.getPackageName()); + } +} diff --git a/quick_android_mvp/src/main/AndroidManifest.xml b/quick_android_mvp/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6415375 --- /dev/null +++ b/quick_android_mvp/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/QuickAndroidMVP.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/QuickAndroidMVP.kt new file mode 100644 index 0000000..64525f1 --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/QuickAndroidMVP.kt @@ -0,0 +1,196 @@ +package cn.qjm253.quick_android_mvp + +import android.app.Application +import cn.qjm253.quick_android_base.QuickAndroid +import cn.qjm253.quick_android_mvp.model.inernet.QuickAndroidMVPAPIManager +import cn.qjm253.quick_android_mvp.model.inernet.rx.QuickAndroidMVPResultDeal +import com.github.anzewei.parallaxbacklayout.ParallaxHelper + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +object QuickAndroidMVP { + + ////////////////////////////////////////// + /////// 初始化操作 + ////////////////////////////////////////// + + /** + * QuickAndroidMVP 模块初始化操作 + * + * @param context Application 对象 + * @param baseUrl 网络请求的前缀, eg.: https://example.cn/ + * @param enablePersistentCookieJar 是否开启Cookie的自动持久化和发送 + */ + fun init( + context: Application, + baseUrl: String = "", + enablePersistentCookieJar: Boolean = true + ): QuickAndroidMVP { + QuickAndroidMVPAPIManager.init(context, enablePersistentCookieJar, baseUrl) + return this + } + + + + + + //////////////////////////////////////////// + //////// 网络请求统一处理 + //////////////////////////////////////////// + val onCompleteListeners = mutableListOf<() -> Unit>() + + + /** + * 添加一个网络请求 onComplete 的拦截监听 + * 每一个使用 Observable.qaSubscribe 进行订阅的网络请求的onComplete执行的时候都会触发 + * + * @see cn.qjm253.quick_android_mvp.model.inernet.rx.qaSubscribe + */ + fun addOnAPICompleteListener(onComplete: () -> Unit): QuickAndroidMVP { + onCompleteListeners.add(onComplete) + return this + } + + + + fun clearOnAPICompleteListeners(): QuickAndroidMVP { + onCompleteListeners.clear() + return this + } + + + val onBeforeNextListeners = mutableListOf<() -> Unit>() + + /** + * 添加一个网络请求 onNext 的拦截监听 + * 每一个使用 Observable.qaSubscribe 进行订阅的网络请求的onNext执行的之前都会触发 + * + * @see cn.qjm253.quick_android_mvp.model.inernet.rx.qaSubscribe + */ + fun addOnAPIBeforeNextListener(onBeforeNextListener: () -> Unit): QuickAndroidMVP { + onBeforeNextListeners.add(onBeforeNextListener) + return this + } + + fun clearOnAPIBeforeNextListeners(): QuickAndroidMVP { + onBeforeNextListeners.clear() + return this + } + + val onErrorListeners = mutableListOf<(Throwable) -> Unit>() + + /** + * 添加一个网络请求 onError 的拦截监听 + * 每一个使用 Observable.qaSubscribe 进行订阅的网络请求的onError执行的时候都会触发 + * + * @see cn.qjm253.quick_android_mvp.model.inernet.rx.qaSubscribe + */ + fun addOnAPIErrorListener(onErrorListener: (Throwable) -> Unit): QuickAndroidMVP { + onErrorListeners.add(onErrorListener) + return this + } + + fun clearOnAPIErrorListeners() { + onErrorListeners.clear() + } + + + ///////////////////////////////////////////////////// + /////////// 网络请求结果统一处理 + ///////////////////////////////////////////////////// + + + var quickAndroidMVPResultDeal: QuickAndroidMVPResultDeal? = null + + /** + * 注册App网络请求结果的统一处理 + * + * @see QuickAndroidMVPResultDeal + */ + fun registerAPIResultDeal(quickAndroidMVPResultDeal: QuickAndroidMVPResultDeal): QuickAndroidMVP { + this.quickAndroidMVPResultDeal = quickAndroidMVPResultDeal + return this + } + +} + + + +/******************************************************* + * 给 QuickAndroid 添加的扩展 + * + * @see cn.qjm253.quick_android_base.QuickAndroid + * + ******************************************************/ + + +/** + * @see QuickAndroidMVP.init + */ +fun QuickAndroid.initMVP( + context: Application, + baseUrl: String = "", + enablePersistentCookieJar: Boolean = true +): QuickAndroid { + QuickAndroidMVP.init(context, baseUrl, enablePersistentCookieJar) + return this +} + +/** + * @see QuickAndroidMVP.addOnAPICompleteListener + */ +fun QuickAndroid.addOnAPICompleteListener(onComplete: () -> Unit): QuickAndroid { + QuickAndroidMVP.addOnAPICompleteListener(onComplete) + return this +} + + +/** + * @see QuickAndroidMVP.clearOnAPICompleteListeners + */ +fun QuickAndroid.clearOnAPICompleteListeners(): QuickAndroid { + QuickAndroidMVP.clearOnAPICompleteListeners() + return this +} + +/** + * @see QuickAndroidMVP.addOnAPIBeforeNextListener + */ +fun QuickAndroid.addOnAPIBeforeNextListener(onBeforeNextListener: () -> Unit): QuickAndroid { + QuickAndroidMVP.addOnAPIBeforeNextListener(onBeforeNextListener) + return this +} + +/** + * @see QuickAndroidMVP.clearOnAPIBeforeNextListeners + */ +fun QuickAndroid.clearOnAPIBeforeNextListeners(): QuickAndroid { + QuickAndroidMVP.clearOnAPIBeforeNextListeners() + return this +} + +/** + * @see QuickAndroidMVP.addOnAPIErrorListener + */ +fun QuickAndroid.addOnAPIErrorListener(onErrorListener: (Throwable) -> Unit): QuickAndroid { + QuickAndroidMVP.addOnAPIErrorListener(onErrorListener) + return this +} + +/** + * @see QuickAndroidMVP.clearOnAPIErrorListeners + */ +fun QuickAndroid.clearOnAPIErrorListeners(): QuickAndroid { + QuickAndroidMVP.clearOnAPIErrorListeners() + return this +} + +/** + * @see QuickAndroidMVP.registerAPIResultDeal + */ +fun QuickAndroid.registerAPIResultDeal(quickAndroidMVPResultDeal: QuickAndroidMVPResultDeal): QuickAndroid { + QuickAndroidMVP.registerAPIResultDeal(quickAndroidMVPResultDeal) + return this +} diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/activity/BaseQuickAndroidMVPActivity.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/activity/BaseQuickAndroidMVPActivity.kt new file mode 100644 index 0000000..93cc1fc --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/activity/BaseQuickAndroidMVPActivity.kt @@ -0,0 +1,69 @@ +package cn.qjm253.quick_android_mvp.base.activity + +import android.os.Bundle +import androidx.lifecycle.Lifecycle +import cn.qjm253.quick_android_base.base.activity.BaseQuickAndroidActivity +import cn.qjm253.quick_android_base.extensions.toast +import cn.qjm253.quick_android_mvp.base.mvp.BasePresenter +import cn.qjm253.quick_android_mvp.base.mvp.BaseView +import com.orhanobut.logger.Logger +//import com.uber.autodispose.AutoDispose +//import com.uber.autodispose.AutoDisposeConverter +//import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider + +/** + * MVP 模式的Activity基类 + * + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +abstract class BaseQuickAndroidMVPActivity

> : BaseQuickAndroidActivity(), BaseView { + + protected lateinit var mPresenter: P + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mPresenter = onCreatePresenter() + } + + + override fun doToast(res: Int, duration: Int) { + toast(res, duration) + } + + override fun doToast(message: String, duration: Int) { + toast(message, duration) + } + + + override fun onError(e: Throwable) { + // 打印错误 + Logger.e(e, e.message ?: "") + } + + + override fun showLoading() { + } + + override fun hideLoading() { + } + + + /** + * RxJava订阅绑定到Activity的生命周期中,触发onDestroy生命周期函数的时候自动取消订阅 +// */ +// override fun bindAutoDispose(): AutoDisposeConverter { +// return AutoDispose.autoDisposable( +// AndroidLifecycleScopeProvider +// .from(this, Lifecycle.Event.ON_DESTROY) +// ) +// } + + + /** + * 创建Presenter的接口,每个实现本类的Activity应该都要实现本方法,返回对应的Presenter + */ + abstract fun onCreatePresenter(): P + + +} \ No newline at end of file diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/fragment/BaseQuickAndroidMVPFragment.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/fragment/BaseQuickAndroidMVPFragment.kt new file mode 100644 index 0000000..4212dfc --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/fragment/BaseQuickAndroidMVPFragment.kt @@ -0,0 +1,63 @@ +package cn.qjm253.quick_android_mvp.base.fragment + +import android.os.Bundle +import androidx.lifecycle.Lifecycle +import cn.qjm253.quick_android_base.base.fragment.BaseQuickAndroidFragment +import cn.qjm253.quick_android_base.extensions.toast +import cn.qjm253.quick_android_mvp.base.mvp.BasePresenter +import cn.qjm253.quick_android_mvp.base.mvp.BaseView +import com.orhanobut.logger.Logger +//import com.uber.autodispose.AutoDispose +//import com.uber.autodispose.AutoDisposeConverter +//import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider + +/** + * MVP 模式的Fragment基类 + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +abstract class BaseQuickAndroidMVPFragment> + : BaseQuickAndroidFragment(), BaseView { + + + + protected lateinit var mPresenter: P + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mPresenter = onCreatePresenter() + } + + /** + * 创建Presenter的接口,每个实现本类的Activity应该都要实现本方法,返回对应的Presenter + */ + abstract fun onCreatePresenter(): P + + + + + override fun onError(e: Throwable) { + Logger.e(e, e.message ?: "") + } + + override fun doToast(message: String, duration: Int) { + context?.toast(message, duration) + } + + override fun doToast(res: Int, duration: Int) { + context?.toast(res, duration) + } + + override fun showLoading() { + } + + override fun hideLoading() { + } + +// override fun bindAutoDispose(): AutoDisposeConverter { +// return AutoDispose.autoDisposable( +// AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY) +// ) +// } +} \ No newline at end of file diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/mvp/BaseModel.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/mvp/BaseModel.kt new file mode 100644 index 0000000..e3df554 --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/mvp/BaseModel.kt @@ -0,0 +1,7 @@ +package cn.qjm253.quick_android_mvp.base.mvp + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +interface BaseModel \ No newline at end of file diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/mvp/BasePresenter.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/mvp/BasePresenter.kt new file mode 100644 index 0000000..a2d9acd --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/mvp/BasePresenter.kt @@ -0,0 +1,10 @@ +package cn.qjm253.quick_android_mvp.base.mvp + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +abstract class BasePresenter { + protected lateinit var mView: V + protected lateinit var mModel: M +} \ No newline at end of file diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/mvp/BaseView.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/mvp/BaseView.kt new file mode 100644 index 0000000..28f6875 --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/base/mvp/BaseView.kt @@ -0,0 +1,43 @@ +package cn.qjm253.quick_android_mvp.base.mvp + +import android.widget.Toast +import androidx.annotation.StringRes +import cn.qjm253.quick_android_mvp.exceptions.MVPExceptionWrapper +//import com.uber.autodispose.AutoDisposeConverter + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +interface BaseView { + + /** + * 通用的错误回调,应在Activity或者Fragment中实现 + */ + fun onError(e: Throwable) + + /** + * 显示吐司弹窗 + */ + fun doToast(message: String, duration: Int = Toast.LENGTH_SHORT) + fun doToast(@StringRes res: Int, duration: Int = Toast.LENGTH_SHORT) + + + /** + * 现在正在加载状态 + */ + fun showLoading() + + /** + * 取消正在加载状态 + */ + fun hideLoading() + + /** + * 绑定Android生命周期 防止RxJava内存泄漏 + * + * @param + * @return + */ +// fun bindAutoDispose(): AutoDisposeConverter +} \ No newline at end of file diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/exceptions/MVPAPIResultDealException.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/exceptions/MVPAPIResultDealException.kt new file mode 100644 index 0000000..a4b765f --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/exceptions/MVPAPIResultDealException.kt @@ -0,0 +1,9 @@ +package cn.qjm253.quick_android_mvp.exceptions + +import java.lang.Exception + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +class MVPAPIResultDealException(msg: String) : Exception(msg) \ No newline at end of file diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/exceptions/MVPAPIResultDealNeedRetryException.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/exceptions/MVPAPIResultDealNeedRetryException.kt new file mode 100644 index 0000000..6150329 --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/exceptions/MVPAPIResultDealNeedRetryException.kt @@ -0,0 +1,9 @@ +package cn.qjm253.quick_android_mvp.exceptions + +import java.lang.Exception + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +class MVPAPIResultDealNeedRetryException(msg: String) : Exception(msg) \ No newline at end of file diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/exceptions/MVPExceptionWrapper.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/exceptions/MVPExceptionWrapper.kt new file mode 100644 index 0000000..f1aa0eb --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/exceptions/MVPExceptionWrapper.kt @@ -0,0 +1,16 @@ +package cn.qjm253.quick_android_mvp.exceptions + +import cn.qjm253.quick_android_base.util.GsonUtil +import java.lang.Exception + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +class MVPExceptionWrapper( + val originException: Exception +) + +fun test() { + +} \ No newline at end of file diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/inernet/QuickAndroidMVPAPIManager.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/inernet/QuickAndroidMVPAPIManager.kt new file mode 100644 index 0000000..89977f7 --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/inernet/QuickAndroidMVPAPIManager.kt @@ -0,0 +1,84 @@ +package cn.qjm253.quick_android_mvp.model.inernet + +import android.app.Application +import com.franmontiel.persistentcookiejar.PersistentCookieJar +import com.franmontiel.persistentcookiejar.cache.SetCookieCache +import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor +import okhttp3.OkHttpClient +import retrofit2.Converter +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import java.nio.charset.Charset +import java.util.concurrent.TimeUnit + + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +object QuickAndroidMVPAPIManager { + + val UTF8 = Charset.forName("UTF-8") + val DEFAULT_CACHE_SIZE = 20 * 1024 * 1024 + val DEFAULT_TIME_OUT = 10L + val services = mutableListOf() + lateinit var client: OkHttpClient + lateinit var baseUrl: String + + + /** + * 网络请求工具类初始化 + */ + fun init( + application: Application, + enablePersistentCookieJar: Boolean = true, // 是否开启自动cookie保存和提交 + baseUrl: String = "" + ) { + val okhttpBuilder = OkHttpClient.Builder() + .connectTimeout(DEFAULT_TIME_OUT, TimeUnit.SECONDS) + .writeTimeout(DEFAULT_TIME_OUT, TimeUnit.SECONDS) + .readTimeout(DEFAULT_TIME_OUT, TimeUnit.SECONDS) + + if (enablePersistentCookieJar) { + okhttpBuilder.cookieJar(PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(application))) + } + + this.baseUrl = baseUrl + client = okhttpBuilder + .build() + } + + /** + * 注册一个服务,支持Json解析 + */ + fun registerService(serviceClass: Class): T { + val newService = createService(serviceClass, GsonConverterFactory.create()) + services.add(newService!!) + return newService + } + + fun getService(serviceClass: Class): T { + var result: T? = null + services.forEach { + if(it as? T != null ) { + result = it + } + } + return result ?: registerService(serviceClass) + } + + fun createService(serviceClass: Class, vararg factory: Converter.Factory): T { + val build = Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + for (aFactory in factory) { + build.addConverterFactory(aFactory) + } + + val retrofit = build + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + return retrofit.create(serviceClass) + } +} \ No newline at end of file diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/inernet/rx/QuickAndroidMVPObserver.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/inernet/rx/QuickAndroidMVPObserver.kt new file mode 100644 index 0000000..e0152d4 --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/inernet/rx/QuickAndroidMVPObserver.kt @@ -0,0 +1,43 @@ +package cn.qjm253.quick_android_mvp.model.inernet.rx + +import androidx.annotation.CallSuper +import cn.qjm253.quick_android_mvp.QuickAndroidMVP +import cn.qjm253.quick_android_mvp.base.mvp.BaseView +import io.reactivex.Observer +import io.reactivex.disposables.Disposable + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ + +abstract class QuickAndroidMVPObserver(private val mView: BaseView) : Observer { + + @CallSuper + override fun onComplete() { + QuickAndroidMVP.onCompleteListeners.forEach { + it() + } + } + + @CallSuper + override fun onNext(t: T) { + QuickAndroidMVP.onBeforeNextListeners.forEach { + it() + } + + } + + @CallSuper + override fun onError(e: Throwable) { + QuickAndroidMVP.onErrorListeners.forEach { + it(e) + } + mView.onError(e) + } + + @CallSuper + override fun onSubscribe(d: Disposable) { + } + +} \ No newline at end of file diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/inernet/rx/QuickAndroidMVPResultDeal.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/inernet/rx/QuickAndroidMVPResultDeal.kt new file mode 100644 index 0000000..f4e2661 --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/inernet/rx/QuickAndroidMVPResultDeal.kt @@ -0,0 +1,69 @@ +package cn.qjm253.quick_android_mvp.model.inernet.rx + +import io.reactivex.Observable + + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ + +abstract class QuickAndroidMVPResultDeal ( + exceptionCodes: Array>, // 错误的状态码对应表 + normalCodes: Array>, // 除错误以外的状态码 + needRetryCodes: Array> // 需要重试的状态码 +) { + + enum class Type { + TYPE_NORMAL, TYPE_ERROR, TYPE_ERROR_NEED_RETRY + } + + class QuickAndroidMVPResultDealItem( + val code: Int, + val description: String, + val type: Type + ) + + + private val exceptionCodeMap: MutableMap = mutableMapOf(*exceptionCodes) + private val normalCodeMap: MutableMap = mutableMapOf(*normalCodes) + private val needRetryCodeMap: MutableMap = mutableMapOf(*needRetryCodes) + + + fun getQuickAndroidMVPResultDealItem(code: Int): QuickAndroidMVPResultDealItem? { + var item: QuickAndroidMVPResultDealItem? = null + when { + exceptionCodeMap.keys.contains(code) -> { + item = QuickAndroidMVPResultDealItem(code, exceptionCodeMap[code] ?: "", Type.TYPE_ERROR) + } + normalCodeMap.keys.contains(code) -> { + item = QuickAndroidMVPResultDealItem(code, normalCodeMap[code] ?: "", Type.TYPE_NORMAL) + } + needRetryCodeMap.keys.contains(code) -> { + item = QuickAndroidMVPResultDealItem(code, needRetryCodeMap[code] ?: "", Type.TYPE_ERROR_NEED_RETRY) + } + } + return item + } + + + /** + * 正常状态码下的统一处理回调 + */ + abstract fun normalDeal(code: Int, description: String) + + + /** + * 错误状态码下的统一处理回调 + */ + abstract fun errorDeal(code: Int, description: String) + + /** + * 处理后重试 + * + * eg.: 应用场景 => Token失效之后重新获取Token,新的Token获取成功后重新发起原先失败的请求 + * @return 返回的Observable触发onComplete 或者 onError, 则不会重新订阅原Observable + * 返回的Observable触发onNext,则会重新订阅原Observable + */ + abstract fun dealRetry(t: Throwable): Observable +} \ No newline at end of file diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/inernet/rx/RxExtensions.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/inernet/rx/RxExtensions.kt new file mode 100644 index 0000000..709dd75 --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/inernet/rx/RxExtensions.kt @@ -0,0 +1,82 @@ +package cn.qjm253.quick_android_mvp.model.inernet.rx + +import cn.qjm253.quick_android_mvp.QuickAndroidMVP +import cn.qjm253.quick_android_mvp.base.mvp.BaseView +import cn.qjm253.quick_android_mvp.exceptions.MVPAPIResultDealException +import cn.qjm253.quick_android_mvp.exceptions.MVPAPIResultDealNeedRetryException +import cn.qjm253.quick_android_mvp.model.response.BaseQuickAndroidResponseBody +import io.reactivex.Observable +import io.reactivex.disposables.Disposable + + +/** + * 网络请求结果统一处理 + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +fun Observable.qaHandleResult(): Observable { + return this.compose { upstream -> + upstream.flatMap { + val code = it.code + + val item = QuickAndroidMVP.quickAndroidMVPResultDeal?.getQuickAndroidMVPResultDealItem(code) + when (item?.type) { + QuickAndroidMVPResultDeal.Type.TYPE_NORMAL -> { + QuickAndroidMVP.quickAndroidMVPResultDeal?.normalDeal(item.code, item.description) + return@flatMap Observable.just(it) + } + QuickAndroidMVPResultDeal.Type.TYPE_ERROR -> { + QuickAndroidMVP.quickAndroidMVPResultDeal?.errorDeal(item.code, item.description) + return@flatMap Observable.error(MVPAPIResultDealException(item.description)) + } + QuickAndroidMVPResultDeal.Type.TYPE_ERROR_NEED_RETRY -> { + return@flatMap Observable.error(MVPAPIResultDealNeedRetryException(item.description)) + } + } + return@flatMap Observable.just(it) + }.retryWhen { t: Observable -> + t.flatMap { + if(QuickAndroidMVP.quickAndroidMVPResultDeal != null && + it is MVPAPIResultDealNeedRetryException) { + return@flatMap QuickAndroidMVP.quickAndroidMVPResultDeal?.dealRetry(it) + } + return@flatMap Observable.error(it) + } + + } + } +} + + +/** + * 网络请求订阅封装 + */ +fun Observable.qaSubscribe( + mView: BaseView, + onNext_: (T) -> Unit, + onError_: (Throwable) -> Unit = {}, + onComplete_: () -> Unit = {}, + onSubscribe_: (Disposable) -> Unit = {} +) { + this.subscribe(object : QuickAndroidMVPObserver(mView) { + override fun onNext(t: T) { + super.onNext(t) + onNext_(t) + } + + override fun onError(e: Throwable) { + super.onError(e) + onError_(e) + } + + override fun onComplete() { + super.onComplete() + onComplete_() + } + + override fun onSubscribe(d: Disposable) { + super.onSubscribe(d) + onSubscribe_(d) + } + }) +} \ No newline at end of file diff --git a/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/response/BaseQuickAndroidResponseBody.kt b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/response/BaseQuickAndroidResponseBody.kt new file mode 100644 index 0000000..db6f0bb --- /dev/null +++ b/quick_android_mvp/src/main/java/cn/qjm253/quick_android_mvp/model/response/BaseQuickAndroidResponseBody.kt @@ -0,0 +1,9 @@ +package cn.qjm253.quick_android_mvp.model.response + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ +open class BaseQuickAndroidResponseBody( + val code: Int +) \ No newline at end of file diff --git a/quick_android_mvp/src/main/res/values/strings.xml b/quick_android_mvp/src/main/res/values/strings.xml new file mode 100644 index 0000000..8542005 --- /dev/null +++ b/quick_android_mvp/src/main/res/values/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/quick_android_mvp/src/test/java/cn/qjm253/quick_android_mvp/ExampleUnitTest.java b/quick_android_mvp/src/test/java/cn/qjm253/quick_android_mvp/ExampleUnitTest.java new file mode 100644 index 0000000..a8a9b64 --- /dev/null +++ b/quick_android_mvp/src/test/java/cn/qjm253/quick_android_mvp/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package cn.qjm253.quick_android_mvp; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/quick_android_qrcode/.gitignore b/quick_android_qrcode/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/quick_android_qrcode/.gitignore @@ -0,0 +1 @@ +/build diff --git a/quick_android_qrcode/README.md b/quick_android_qrcode/README.md new file mode 100644 index 0000000..3f01e17 --- /dev/null +++ b/quick_android_qrcode/README.md @@ -0,0 +1,51 @@ +> ## quick_android_qrcode 模块说明文档 +> +> 本模块基于[**BGAQRCode-Android**](https://github.com/bingoogolapple/BGAQRCode-Android)进行进一步的封装,支持使用RxJava链式调用的方式进行扫码并获得扫码的结果(不用复写`onActivityResult`来手动获取结果) + + + +- ### 基本使用 + +```kotlin +QuickAndroidQrCode + .create(context) // context 可以是Activity或者Fragment + .scanQrCode() + .subscribe({qrCodeResult-> // 扫码成功 + + }, { err-> // 扫码失败(权限被拒绝,摄像头打开失败等) + + }) + +// 当然,如果使用Kotlin开发也可以使用下面的统一扩展接口 +QuickAndroid + .scanQrCode(context) + .subscribe({qrCodeResult-> // 扫码成功 + + }, { err-> // 扫码失败(权限被拒绝,摄像头打开失败等) + + }) +``` + +- ### 进阶 + - **修改返回按钮的样式** + ```kotlin + QuickAndroidQrCode + .create(context) // context 可以是Activity或者Fragment + .setBackIconRes(R.drawable.back) // 设置返回按钮的资源 + .setBackIconSize(30f) // 设置返回按钮的大小 + .setBackIconMargin(top, right, bottom, left) // 设置返回按钮的边距 + .scanQrCode() + .subscribe({qrCodeResult-> // 扫码成功 + + }, { err-> // 扫码失败(权限被拒绝,摄像头打开失败等) + + }) + ``` + + + +- ### 模块说明 + + - 本模块提供了一个简单的[QuickAndroidQrCodeActivity](https://github.com/SunnyQjm/quickandroid/blob/master/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/activity/QuickAndroidQrCodeActivity.kt)实现扫码界面,可以直接使用 + - 如果希望自定义扫码界面,也可以参考[QuickAndroidQrCodeActivity](https://github.com/SunnyQjm/quickandroid/blob/master/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/activity/QuickAndroidQrCodeActivity.kt)复用本模块提供的[QuickAndroidQrCodeFragment](https://github.com/SunnyQjm/quickandroid/blob/master/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/fragment/QuickAndroidQrCodeFragment.kt)来实现自定义的界面 + diff --git a/quick_android_qrcode/build.gradle b/quick_android_qrcode/build.gradle new file mode 100644 index 0000000..cc041a3 --- /dev/null +++ b/quick_android_qrcode/build.gradle @@ -0,0 +1,42 @@ +apply plugin: 'com.android.library' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion compile_sdk_version + + defaultConfig { + minSdkVersion min_sdk_version + targetSdkVersion target_sdk_version + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "androidx.appcompat:appcompat:${appcompact_version}" + testImplementation "junit:junit:${junit_version}" + androidTestImplementation "androidx.test:runner:${test_runner_version}" + androidTestImplementation "androidx.test.espresso:espresso-core:${espresso_core_version}" + + implementation project(path: ':quick_android_base') + + // zxing 二维码识别和生成 + implementation "cn.bingoogolapple:bga-qrcode-zxing:${bag_qrcode_zxing_version}" +} diff --git a/quick_android_qrcode/proguard-rules.pro b/quick_android_qrcode/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/quick_android_qrcode/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/quick_android_qrcode/src/androidTest/java/cn/qjm253/quick_android_qrcode/ExampleInstrumentedTest.java b/quick_android_qrcode/src/androidTest/java/cn/qjm253/quick_android_qrcode/ExampleInstrumentedTest.java new file mode 100644 index 0000000..c7240b6 --- /dev/null +++ b/quick_android_qrcode/src/androidTest/java/cn/qjm253/quick_android_qrcode/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package cn.qjm253.quick_android_qrcode; + +import android.content.Context; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("cn.qjm253.quick_android_qrcode.test", appContext.getPackageName()); + } +} diff --git a/quick_android_qrcode/src/main/AndroidManifest.xml b/quick_android_qrcode/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c3c81da --- /dev/null +++ b/quick_android_qrcode/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/QuickAndroidQrCode.kt b/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/QuickAndroidQrCode.kt new file mode 100644 index 0000000..fa2f510 --- /dev/null +++ b/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/QuickAndroidQrCode.kt @@ -0,0 +1,131 @@ +package cn.qjm253.quick_android_qrcode + +import androidx.annotation.DrawableRes +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import cn.qjm253.quick_android_base.QuickAndroid +import cn.qjm253.quick_android_base.extensions.getLazySingleton +import cn.qjm253.quick_android_base.util.RxSchedulersHelper +import cn.qjm253.quick_android_qrcode.activity.QuickAndroidQrCodeActivity +import cn.qjm253.quick_android_qrcode.fragment.QuickAndroidQrCodeTransFragment +import cn.qjm253.quick_android_qrcode.model.QrCodeResult +import io.reactivex.Observable + +/** + * @author SunnyQjm + * @date 19-7-7 下午10:11 + */ + +class QuickAndroidQrCode { + + private var mQrCodeTransFragment: Lazy + private var backIconRes: Int = R.drawable.quick_android_qr_code_default_back + private var backIconSize: Float = 30f + private var backIconTopMargin: Float = 15f + private var backIconRightMargin: Float = 0f + private var backIconBottomMargin: Float = 0f + private var backIconLeftMargin: Float = 15f + + companion object { + const val TAG = "QuickAndroidQrCode" + + fun create(activity: FragmentActivity): QuickAndroidQrCode { + return QuickAndroidQrCode(activity) + } + + fun create(fragment: Fragment): QuickAndroidQrCode { + return QuickAndroidQrCode(fragment) + } + } + + constructor(activity: FragmentActivity) { + mQrCodeTransFragment = activity.supportFragmentManager + .getLazySingleton(TAG, QuickAndroidQrCodeTransFragment::class.java) + } + + constructor(fragment: Fragment) { + mQrCodeTransFragment = fragment.childFragmentManager + .getLazySingleton(TAG, QuickAndroidQrCodeTransFragment::class.java) + } + + /** + * 设置返回按钮的资源 + */ + fun setBackIconRes(@DrawableRes icon: Int): QuickAndroidQrCode { + backIconRes = icon + return this + } + + /** + * 设置返回按钮的大小 + * ps: 单位为dp + */ + fun setBackIconSize(size: Float): QuickAndroidQrCode { + backIconSize = size + return this + } + + /** + * 设置返回按钮的边距 + * 1. 单位为dp + * 2. 代码中已经自动去掉了状态栏的高度,不用考虑状态栏 + */ + fun setBackIconMargin(top: Float, right: Float, bottom: Float, left: Float): QuickAndroidQrCode { + backIconTopMargin = top + backIconRightMargin = right + backIconBottomMargin = bottom + backIconLeftMargin = left + return this + } + + /** + * 跳转到扫码界面,并返回一个Observable对象用于接收扫码结果 + */ + fun scanQrCode(): Observable { + return mQrCodeTransFragment.value + .setBackIconRes(backIconRes) + .setBackIconSize(backIconSize) + .setBackIconMargin( + backIconTopMargin, + backIconRightMargin, + backIconBottomMargin, + backIconLeftMargin + ) + .scanCode(QuickAndroidQrCodeActivity::class.java) + .compose(RxSchedulersHelper.io_main()) + } +} + + +////////////////////////////////////////////////////////// +////////// QuickAndroid 扩展 +////////////////////////////////////////////////////////// + +fun QuickAndroid.scanQrCode(activity: FragmentActivity): Observable { + return QuickAndroidQrCode.create(activity) + .scanQrCode() +} + +fun QuickAndroid.scanQrCode(fragment: Fragment): Observable { + return QuickAndroidQrCode.create(fragment) + .scanQrCode() +} + + +////////////////////////////////////////////////////////// +////////// FragmentActivity 扩展 +////////////////////////////////////////////////////////// + +fun FragmentActivity.scanCode(): Observable { + return QuickAndroidQrCode.create(this) + .scanQrCode() +} + +////////////////////////////////////////////////////////// +////////// Fragment 扩展 +////////////////////////////////////////////////////////// + +fun Fragment.scanCode(): Observable { + return QuickAndroidQrCode.create(this) + .scanQrCode() +} \ No newline at end of file diff --git a/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/activity/QuickAndroidQrCodeActivity.kt b/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/activity/QuickAndroidQrCodeActivity.kt new file mode 100644 index 0000000..68c0632 --- /dev/null +++ b/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/activity/QuickAndroidQrCodeActivity.kt @@ -0,0 +1,32 @@ +package cn.qjm253.quick_android_qrcode.activity + +import android.os.Bundle +import cn.qjm253.quick_android_base.base.activity.BaseQuickAndroidActivity +import cn.qjm253.quick_android_base.extensions.addFragmentToActivity +import cn.qjm253.quick_android_qrcode.R +import cn.qjm253.quick_android_qrcode.fragment.QuickAndroidQrCodeFragment +import com.gyf.immersionbar.BarHide +import com.gyf.immersionbar.ImmersionBar + +class QuickAndroidQrCodeActivity : BaseQuickAndroidActivity() { + + companion object { + const val QR_CODE_RESULT = QuickAndroidQrCodeFragment.QR_CODE_RESULT + const val QR_CODE_ERROR = QuickAndroidQrCodeFragment.QR_CODE_ERROR + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ImmersionBar.with(this) + .fullScreen(true) + .hideBar(BarHide.FLAG_HIDE_NAVIGATION_BAR) + .init() + setContentView(R.layout.activity_quick_android_qr_code) + + // 如果有的话隐藏ActionBar + supportActionBar?.hide() + addFragmentToActivity(R.id.quickAndroidQrCodeContainer) { + return@addFragmentToActivity QuickAndroidQrCodeFragment.newInstance() + } + } +} diff --git a/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/fragment/QuickAndroidQrCodeFragment.kt b/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/fragment/QuickAndroidQrCodeFragment.kt new file mode 100644 index 0000000..6366496 --- /dev/null +++ b/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/fragment/QuickAndroidQrCodeFragment.kt @@ -0,0 +1,132 @@ +package cn.qjm253.quick_android_qrcode.fragment + +import android.content.Intent +import android.widget.RelativeLayout +import cn.bingoogolapple.qrcode.core.QRCodeView +import cn.qjm253.quick_android_base.base.fragment.BaseQuickAndroidFragment +import cn.qjm253.quick_android_base.extensions.e +import cn.qjm253.quick_android_base.extensions.i +import cn.qjm253.quick_android_base.extensions.vibrator +import cn.qjm253.quick_android_base.params.IntentParam +import cn.qjm253.quick_android_base.util.DisplayUtils +import cn.qjm253.quick_android_qrcode.R +import com.gyf.immersionbar.ImmersionBar +import kotlinx.android.synthetic.main.fragment_quick_android_qrcode.* + + +/** + * @author SunnyQjm + * @date 19-7-7 上午8:43 + */ + +class QuickAndroidQrCodeFragment : BaseQuickAndroidFragment() { + + companion object { + const val QR_CODE_RESULT = "QR_CODE_RESULT" + const val QR_CODE_ERROR = "QR_CODE_ERROR" + const val QR_CODE_BACK_ICON_RES = "QR_CODE_BACK_ICON_RES" + const val QR_CODE_BACK_ICON_SIZE = "QR_CODE_BACK_ICON_SIZE" + const val QR_CODE_BAKC_ICON_MARGIN = "QR_CODE_BAKC_ICON_MARGIN" + + fun newInstance(intentParam: IntentParam? = null): QuickAndroidQrCodeFragment { + return newInstance_(QuickAndroidQrCodeFragment::class.java, intentParam) + } + } + + override fun getRes(): Int = R.layout.fragment_quick_android_qrcode + + override fun initView() { + zxingview.setDelegate(object : QRCodeView.Delegate { + override fun onScanQRCodeSuccess(result: String?) { + // 识别成功,震动 + activity?.vibrator() + + Intent() + .putExtra(QR_CODE_RESULT, result).let { + activity?.setResult(0, it) + activity?.finish() + } + + } + + override fun onCameraAmbientBrightnessChanged(isDark: Boolean) { +// "onCameraAmbientBrightnessChanged => isDark: $isDark".i() + } + + override fun onScanQRCodeOpenCameraError() { + Intent() + .putExtra(QR_CODE_RESULT, "打开相机失败").let { + activity?.setResult(0, it) + activity?.finish() + } + "打开相机失败".e() + } + + }) + + val statusBarHeight = ImmersionBar.getStatusBarHeight(this); + + activity?.intent?.let { + val backIconRes = + it.getIntExtra(QR_CODE_BACK_ICON_RES, R.drawable.quick_android_qr_code_default_back) + val backIconSize = it.getFloatExtra(QR_CODE_BACK_ICON_SIZE, 30f) + val backIconMargin: FloatArray = + it.getFloatArrayExtra(QR_CODE_BAKC_ICON_MARGIN) ?: floatArrayOf(15f, 0f, 0f, 15f) + RelativeLayout.LayoutParams(imgBack.layoutParams).let { lp -> + lp.setMargins( + DisplayUtils.dip2px(context, backIconMargin[3]), + DisplayUtils.dip2px(context, backIconMargin[0]) + statusBarHeight, + DisplayUtils.dip2px(context, backIconMargin[1]), + DisplayUtils.dip2px(context, backIconMargin[2]) + ) + lp.width = DisplayUtils.dip2px(context, backIconSize) + lp.height = DisplayUtils.dip2px(context, backIconSize) + imgBack.layoutParams = lp + } + imgBack.setImageResource(backIconRes) + } + + imgBack.setOnClickListener { + activity?.onBackPressed() + } + + requestCamera() + } + + + override fun onPermissionsGranted(requestCode: Int, perms: MutableList) { + super.onPermissionsGranted(requestCode, perms) + + // 打开后摄像头开始预览,但是并没有开始识别 + zxingview.startCamera() + + // 显示扫描框,并开始识别 + zxingview.startSpotAndShowRect() + } + + + override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { + super.onPermissionsDenied(requestCode, perms) + Intent() + .putExtra(QR_CODE_RESULT, "获取相机权限失败").let { + activity?.setResult(0, it) + activity?.finish() + } + } + + override fun onStop() { + super.onStop() + zxingview.stopCamera() + } + + override fun onDestroy() { + zxingview?.onDestroy() + super.onDestroy() + } + + + override fun initialLoadData() { + } + + +} \ No newline at end of file diff --git a/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/fragment/QuickAndroidQrCodeTransFragment.kt b/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/fragment/QuickAndroidQrCodeTransFragment.kt new file mode 100644 index 0000000..0ec7e3c --- /dev/null +++ b/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/fragment/QuickAndroidQrCodeTransFragment.kt @@ -0,0 +1,114 @@ +package cn.qjm253.quick_android_qrcode.fragment + +import android.content.Intent +import android.os.Bundle +import androidx.annotation.DrawableRes +import androidx.fragment.app.Fragment +import cn.qjm253.quick_android_base.base.activity.BaseQuickAndroidActivity +import cn.qjm253.quick_android_base.extensions.e +import cn.qjm253.quick_android_base.extensions.i +import cn.qjm253.quick_android_base.extensions.jumpForResult +import cn.qjm253.quick_android_base.extensions.toJson +import cn.qjm253.quick_android_base.params.IntentParam +import cn.qjm253.quick_android_qrcode.R +import cn.qjm253.quick_android_qrcode.model.QrCodeResult +import io.reactivex.Observable +import io.reactivex.ObservableEmitter + +/** + * 这个Fragment用于跳转到扫码界面,并接收其返回的结果,然后使用RxJava进行封装,便于链式调用 + * + * @author SunnyQjm + * @date 19-7-8 下午3:22 + */ +class QuickAndroidQrCodeTransFragment : Fragment() { + + private var emitter: ObservableEmitter? = null + + private var backIconRes: Int = R.drawable.quick_android_qr_code_default_back + private var backIconSize: Float = 30f + private var backIconTopMargin: Float = 15f + private var backIconRightMargin: Float = 0f + private var backIconBottomMargin: Float = 0f + private var backIconLeftMargin: Float = 15f + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + retainInstance = true + } + + /** + * 设置返回按钮的资源 + */ + fun setBackIconRes(@DrawableRes icon: Int): QuickAndroidQrCodeTransFragment { + backIconRes = icon + return this + } + + /** + * 设置返回按钮的大小 + * ps: 单位为dp + */ + fun setBackIconSize(size: Float): QuickAndroidQrCodeTransFragment { + backIconSize = size + return this + } + + /** + * 设置返回按钮的边距 + * 1. 单位为dp + * 2. 代码中已经自动去掉了状态栏的高度,不用考虑状态栏 + */ + fun setBackIconMargin( + top: Float, + right: Float, + bottom: Float, + left: Float + ): QuickAndroidQrCodeTransFragment { + backIconTopMargin = top + backIconRightMargin = right + backIconBottomMargin = bottom + backIconLeftMargin = left + return this + } + + fun scanCode(clazz: Class): Observable { + return Observable.create { emitter -> + // 保存事件发送器 + this.emitter = emitter + jumpForResult( + clazz, 0, IntentParam() + .add(QuickAndroidQrCodeFragment.QR_CODE_BACK_ICON_RES, backIconRes) + .add(QuickAndroidQrCodeFragment.QR_CODE_BACK_ICON_SIZE, backIconSize) + .add( + QuickAndroidQrCodeFragment.QR_CODE_BAKC_ICON_MARGIN, floatArrayOf( + backIconTopMargin, backIconRightMargin, + backIconBottomMargin, backIconLeftMargin + ) + ) + ) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + 0 -> { // 扫码结果处理 + data?.let { + val qrCodeResult = it.getStringExtra(QuickAndroidQrCodeFragment.QR_CODE_RESULT) + val qrCodeError = it.getStringExtra(QuickAndroidQrCodeFragment.QR_CODE_ERROR) + qrCodeResult?.i() + qrCodeError?.e() + when { + qrCodeResult != null -> this.emitter?.onNext(QrCodeResult(qrCodeResult)) + qrCodeError != null -> this.emitter?.onError(Throwable(qrCodeError)) + else -> { + + } + } + this.emitter?.onComplete() + } + } + } + } +} \ No newline at end of file diff --git a/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/model/QrCodeResult.kt b/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/model/QrCodeResult.kt new file mode 100644 index 0000000..87853a0 --- /dev/null +++ b/quick_android_qrcode/src/main/java/cn/qjm253/quick_android_qrcode/model/QrCodeResult.kt @@ -0,0 +1,9 @@ +package cn.qjm253.quick_android_qrcode.model + +/** + * @author SunnyQjm + * @date 19-7-8 下午3:32 + */ +class QrCodeResult ( + val content: String +) \ No newline at end of file diff --git a/quick_android_qrcode/src/main/res/drawable/quick_android_qr_code_default_back.png b/quick_android_qrcode/src/main/res/drawable/quick_android_qr_code_default_back.png new file mode 100644 index 0000000..cab12e2 Binary files /dev/null and b/quick_android_qrcode/src/main/res/drawable/quick_android_qr_code_default_back.png differ diff --git a/quick_android_qrcode/src/main/res/layout/activity_quick_android_qr_code.xml b/quick_android_qrcode/src/main/res/layout/activity_quick_android_qr_code.xml new file mode 100644 index 0000000..22ac660 --- /dev/null +++ b/quick_android_qrcode/src/main/res/layout/activity_quick_android_qr_code.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/quick_android_qrcode/src/main/res/layout/fragment_quick_android_qrcode.xml b/quick_android_qrcode/src/main/res/layout/fragment_quick_android_qrcode.xml new file mode 100644 index 0000000..e444d95 --- /dev/null +++ b/quick_android_qrcode/src/main/res/layout/fragment_quick_android_qrcode.xml @@ -0,0 +1,38 @@ + + + + + + + + + + \ No newline at end of file diff --git a/quick_android_qrcode/src/main/res/values/strings.xml b/quick_android_qrcode/src/main/res/values/strings.xml new file mode 100644 index 0000000..8542005 --- /dev/null +++ b/quick_android_qrcode/src/main/res/values/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/quick_android_qrcode/src/test/java/cn/qjm253/quick_android_qrcode/ExampleUnitTest.java b/quick_android_qrcode/src/test/java/cn/qjm253/quick_android_qrcode/ExampleUnitTest.java new file mode 100644 index 0000000..5486f0f --- /dev/null +++ b/quick_android_qrcode/src/test/java/cn/qjm253/quick_android_qrcode/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package cn.qjm253.quick_android_qrcode; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/quick_android_rx_permission/.gitignore b/quick_android_rx_permission/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/quick_android_rx_permission/.gitignore @@ -0,0 +1 @@ +/build diff --git a/quick_android_rx_permission/README.md b/quick_android_rx_permission/README.md new file mode 100644 index 0000000..510bc39 --- /dev/null +++ b/quick_android_rx_permission/README.md @@ -0,0 +1,37 @@ +> ## quick_android_rx_permission 模块说明文档 +> +> 本模块基于[easypermissions](https://github.com/googlesamples/easypermissions)进行封装,实现了RxJava链式调用进行动态权限请求,不需要显示的复写权限请求回调,请求逻辑和请求结果可以在同一代码段,不用分开编写。(封装实现时参考了[RxPermissions](https://github.com/tbruyelle/RxPermissions)项目) + + + +- ### 使用示例 + +```kotlin +QuickAndroidRxPermission + .create(context) // 这个context可以是Activity,也可以是Fragment + .request( + arrayof(Manifest.android.CAMERA) + ) + .subscribe { + if(it.granted) { // 权限请求通过 + + } else { // 权限请求拒绝 + + } + } + +// 当然,如果使用Kotlin开发也可以使用下面的统一扩展接口 +QuickAndroid + .createRxPermission(context) + .request( + arrayof(Manifest.android.CAMERA) + ) + .subscribe { + if(it.granted) { // 权限请求通过 + + } else { // 权限请求拒绝 + + } + } +``` + diff --git a/quick_android_rx_permission/build.gradle b/quick_android_rx_permission/build.gradle new file mode 100644 index 0000000..be7d677 --- /dev/null +++ b/quick_android_rx_permission/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion compile_sdk_version + + defaultConfig { + minSdkVersion min_sdk_version + targetSdkVersion target_sdk_version + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + + implementation "androidx.appcompat:appcompat:${appcompact_version}" + testImplementation "junit:junit:${junit_version}" + androidTestImplementation "androidx.test:runner:${test_runner_version}" + androidTestImplementation "androidx.test.espresso:espresso-core:${espresso_core_version}" + + implementation project(path: ':quick_android_base') +} diff --git a/quick_android_rx_permission/proguard-rules.pro b/quick_android_rx_permission/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/quick_android_rx_permission/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/quick_android_rx_permission/src/androidTest/java/cn/qjm253/quick_android_rx_permission/ExampleInstrumentedTest.java b/quick_android_rx_permission/src/androidTest/java/cn/qjm253/quick_android_rx_permission/ExampleInstrumentedTest.java new file mode 100644 index 0000000..36cd7a1 --- /dev/null +++ b/quick_android_rx_permission/src/androidTest/java/cn/qjm253/quick_android_rx_permission/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package cn.qjm253.quick_android_rx_permission; + +import android.content.Context; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("cn.qjm253.quick_android_rx_permission.test", appContext.getPackageName()); + } +} diff --git a/quick_android_rx_permission/src/main/AndroidManifest.xml b/quick_android_rx_permission/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3d9ae9b --- /dev/null +++ b/quick_android_rx_permission/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/quick_android_rx_permission/src/main/java/cn/qjm253/quick_android_rx_permission/QuickAndroidRxPermission.kt b/quick_android_rx_permission/src/main/java/cn/qjm253/quick_android_rx_permission/QuickAndroidRxPermission.kt new file mode 100644 index 0000000..139bbbd --- /dev/null +++ b/quick_android_rx_permission/src/main/java/cn/qjm253/quick_android_rx_permission/QuickAndroidRxPermission.kt @@ -0,0 +1,65 @@ +package cn.qjm253.quick_android_rx_permission + +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import cn.qjm253.quick_android_base.QuickAndroid +import cn.qjm253.quick_android_base.extensions.getLazySingleton +import cn.qjm253.quick_android_base.util.RxSchedulersHelper +import io.reactivex.Observable + +/** + * @author SunnyQjm + * @date 19-7-8 上午9:03 + */ + +class QuickAndroidRxPermission { + companion object { + const val TAG = "QuickAndroidRxPermission" + + fun create(activity: FragmentActivity): QuickAndroidRxPermission { + return QuickAndroidRxPermission(activity) + } + + fun create(fragment: Fragment): QuickAndroidRxPermission { + return QuickAndroidRxPermission(fragment) + } + } + + private var mRxPermissionFragment: Lazy + private var requestCode = 0 + + constructor(activity: FragmentActivity) { + mRxPermissionFragment = activity.supportFragmentManager + .getLazySingleton(TAG, QuickAndroidRxPermissionFragment::class.java) + } + + constructor(fragment: Fragment) { + mRxPermissionFragment = fragment.childFragmentManager + .getLazySingleton(TAG, QuickAndroidRxPermissionFragment::class.java) + } + + + fun request(permissions: Array, @StringRes tip: Int): Observable { + requestCode++ + return mRxPermissionFragment.value.requestPermissions(permissions, tip, requestCode) + .compose(RxSchedulersHelper.io_main()) + } + + fun request(permissions: Array, tip: String = "请求权限"): Observable { + requestCode++ + return mRxPermissionFragment.value.requestPermissions(permissions, tip, requestCode) + .compose(RxSchedulersHelper.io_main()) + } +} + +////////////////////////////////////////////////////////// +////////// QuickAndroid 扩展 +////////////////////////////////////////////////////////// +fun QuickAndroid.createRxPermission(activity: FragmentActivity): QuickAndroidRxPermission { + return QuickAndroidRxPermission.create(activity) +} + +fun QuickAndroid.createRxPermission(fragment: Fragment): QuickAndroidRxPermission { + return QuickAndroidRxPermission.create(fragment) +} \ No newline at end of file diff --git a/quick_android_rx_permission/src/main/java/cn/qjm253/quick_android_rx_permission/QuickAndroidRxPermissionFragment.kt b/quick_android_rx_permission/src/main/java/cn/qjm253/quick_android_rx_permission/QuickAndroidRxPermissionFragment.kt new file mode 100644 index 0000000..e4b5033 --- /dev/null +++ b/quick_android_rx_permission/src/main/java/cn/qjm253/quick_android_rx_permission/QuickAndroidRxPermissionFragment.kt @@ -0,0 +1,77 @@ +package cn.qjm253.quick_android_rx_permission + +import android.os.Bundle +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import io.reactivex.Observable +import io.reactivex.ObservableEmitter +import pub.devrel.easypermissions.EasyPermissions + +/** + * @author SunnyQjm + * @date 19-7-8 上午9:13 + */ + +class QuickAndroidRxPermissionFragment : Fragment(), EasyPermissions.PermissionCallbacks { + + private val permissionsMap = + mutableMapOf>() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + retainInstance = true + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) + } + + /** + * 授权成功 + */ + override fun onPermissionsGranted(requestCode: Int, perms: MutableList) { + permissionsMap[requestCode]?.let { + it.onNext(QuickAndroidRxPermissionItem(true, requestCode, perms)) + it.onComplete() + permissionsMap.remove(requestCode) + } + } + + /** + * 授权失败 + */ + override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { + permissionsMap[requestCode]?.let { + it.onNext(QuickAndroidRxPermissionItem(false, requestCode, perms)) + it.onComplete() + permissionsMap.remove(requestCode) + } + } + + + /** + * 请求权限 + */ + fun requestPermissions(permissions: Array, @StringRes tip: Int, requestCode: Int) + : Observable { + return requestPermissions(permissions, getString(tip), requestCode) + } + + + fun requestPermissions(permissions: Array, tip: String, requestCode: Int) + : Observable { + return Observable.create { emitter -> + permissionsMap[requestCode] = emitter + activity?.let { + if (EasyPermissions.hasPermissions(it, *permissions)) { + onPermissionsGranted(requestCode, permissions.toMutableList()) + } else { + EasyPermissions.requestPermissions(this, tip, requestCode, *permissions) + } + } + } + } + +} \ No newline at end of file diff --git a/quick_android_rx_permission/src/main/java/cn/qjm253/quick_android_rx_permission/QuickAndroidRxPermissionItem.kt b/quick_android_rx_permission/src/main/java/cn/qjm253/quick_android_rx_permission/QuickAndroidRxPermissionItem.kt new file mode 100644 index 0000000..eaa55e9 --- /dev/null +++ b/quick_android_rx_permission/src/main/java/cn/qjm253/quick_android_rx_permission/QuickAndroidRxPermissionItem.kt @@ -0,0 +1,12 @@ +package cn.qjm253.quick_android_rx_permission + +/** + * @author SunnyQjm + * @date 19-7-8 上午9:34 + */ + +class QuickAndroidRxPermissionItem( + val granted: Boolean, + val requestCode: Int, + val perms: MutableList +) \ No newline at end of file diff --git a/quick_android_rx_permission/src/main/res/values/strings.xml b/quick_android_rx_permission/src/main/res/values/strings.xml new file mode 100644 index 0000000..8542005 --- /dev/null +++ b/quick_android_rx_permission/src/main/res/values/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/quick_android_rx_permission/src/test/java/cn/qjm253/quick_android_rx_permission/ExampleUnitTest.java b/quick_android_rx_permission/src/test/java/cn/qjm253/quick_android_rx_permission/ExampleUnitTest.java new file mode 100644 index 0000000..7bb747a --- /dev/null +++ b/quick_android_rx_permission/src/test/java/cn/qjm253/quick_android_rx_permission/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package cn.qjm253.quick_android_rx_permission; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/quick_android_webview/.gitignore b/quick_android_webview/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/quick_android_webview/.gitignore @@ -0,0 +1 @@ +/build diff --git a/quick_android_webview/README.md b/quick_android_webview/README.md new file mode 100644 index 0000000..41ed848 --- /dev/null +++ b/quick_android_webview/README.md @@ -0,0 +1,117 @@ +> ## quick_android_webview 模块说明文档 +> +> 本模块提供一个简单的**H5容器**实现方案。 +> +> - 接入腾讯的[X5浏览器](https://x5.tencent.com/)内核,比原生的更安全且对js的新特性有更好的支持,兼容性问题较少,同时支持和QQ、微信等共享内核,可以大大减少APP包的体积。 +> - 本模块实现一个简单的 `Android Webview Bridge` **QuickAndroidWebBridge**,可以实现js调用原生的扫码、图片选择、获取设备信息等功能 + + + +- ### 使用说明 + + - 首先在你的作为H5容器的Acivity中使用 `QuickAndroidWebview` 替代原生的Webview进行布局 + + ```xml + + + + ``` + + - 接着在Activity中使用 + + ```kotlin + // 初始化操作,绑定通信桥 + QuickAndroidWV + .init(this, quickAndroidWebview) + + // 加载URL + quickAndroidWebview.loadUrl("file:///android_asset/www/test.html") + ``` + + - 完成上面两步,基本上H5容器就可以使用了,H5端搭配使用[quick_android_web_bridge](https://github.com/SunnyQjm/quick_android_web_bridge) 这一JS库便可进行原生功能的调用 + +- ### 提供的原生调用能力如下: + + > H5端具体如何接入,参考[quick_android_web_bridge](https://github.com/SunnyQjm/quick_android_web_bridge) + + - #### toast(吐司弹窗) + + H5端可以使用[quick_android_web_bridge](https://github.com/SunnyQjm/quick_android_web_bridge)调用原生的toast + + ```javascript + /** + * 调用Android原生的toast接口 + * + * @param msg 要显示的信息 + */ + function toast(msg: string) + + // 使用示例 + QuickAndroidWebBridge.scanCode() + ``` + + - #### scanCode(扫码) + + H5端可以使用[quick_android_web_bridge](https://github.com/SunnyQjm/quick_android_web_bridge)调用原生的扫码功能,并得到扫码的结果 + + ```javascript + /** + * 调用原生 的扫码功能,并得到扫码的结果 + * + * @return 返回一个Promise对象,可以异步的得到扫码结果 + */ + function scanCode(): Promise + + + + // 使用示例 + QuickAndroidWebBridge.scanCode() + .then(result => { // 扫码成功,在此处理扫码的结果 + + }) + .catch(err => { // 发生错误,在此处理错误 + + }) + ``` + + - #### getSystemInfo(获取Android设备的信息) + + H5端可以使用[quick_android_web_bridge](https://github.com/SunnyQjm/quick_android_web_bridge)获取到当前Android手机的设备信息 + + ```javascript + /** + * 获取当前手机系统的信息 + * + * @return 返回一个Promise对象,可以异步得到扫码结果 + */ + function getSystemInfo(): Promise + + // 使用示例 + QuickAndroidWebBridge.getSystemInfo() + .then(result => { // 获取系统信息成功,在此处理结果 + + }) + .catch(err => { // 发生错误,在此处理错误 + + }) + ``` + + + + - 获取到的信息如下所示: + + | 属性名 | 属性类型 | 属性说明 | + | :-------------: | :------: | :------------------------: | + | brand | string | 手机品牌 | + | model | string | 手机型号 | + | hardware | string | 手机硬件信息 | + | sdkInt | number | Android SDK 版本 | + | release | string | 手机系统版本号 | + | statusBarHeight | number | 手机系统状态栏的高度(px) | + | systemLanguage | string | 当前手机上使用的语言 | + + + diff --git a/quick_android_webview/build.gradle b/quick_android_webview/build.gradle new file mode 100644 index 0000000..440f20f --- /dev/null +++ b/quick_android_webview/build.gradle @@ -0,0 +1,53 @@ +apply plugin: 'com.android.library' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +apply plugin: 'kotlin-kapt' +android { + compileSdkVersion compile_sdk_version + + defaultConfig { + minSdkVersion min_sdk_version + targetSdkVersion target_sdk_version + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + ndk { + abiFilters "armeabi", "armeabi-v7a", "x86", "mips" + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation "androidx.appcompat:appcompat:${appcompact_version}" + testImplementation "junit:junit:${junit_version}" + androidTestImplementation "androidx.test:runner:${test_runner_version}" + androidTestImplementation "androidx.test.espresso:espresso-core:${espresso_core_version}" + + implementation project(path: ':quick_android_base') + implementation project(path: ':quick_android_rx_permission') + implementation project(path: ':quick_android_image_picker') + implementation project(path: ':quick_android_qrcode') + api files('libs/tbs_sdk.jar') + implementation "org.jetbrains.anko:anko-commons:${anko_version}" + +} diff --git a/quick_android_webview/libs/armeabi/liblbs.so b/quick_android_webview/libs/armeabi/liblbs.so new file mode 100644 index 0000000..b558c45 Binary files /dev/null and b/quick_android_webview/libs/armeabi/liblbs.so differ diff --git a/quick_android_webview/libs/tbs_sdk.jar b/quick_android_webview/libs/tbs_sdk.jar new file mode 100644 index 0000000..79c58bd Binary files /dev/null and b/quick_android_webview/libs/tbs_sdk.jar differ diff --git a/quick_android_webview/proguard-rules.pro b/quick_android_webview/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/quick_android_webview/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/quick_android_webview/src/androidTest/java/cn/qjm253/quick_android_webview/ExampleInstrumentedTest.java b/quick_android_webview/src/androidTest/java/cn/qjm253/quick_android_webview/ExampleInstrumentedTest.java new file mode 100644 index 0000000..0163320 --- /dev/null +++ b/quick_android_webview/src/androidTest/java/cn/qjm253/quick_android_webview/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package cn.qjm253.quick_android_webview; + +import android.content.Context; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("cn.qjm253.quick_android_webview.test", appContext.getPackageName()); + } +} diff --git a/quick_android_webview/src/main/AndroidManifest.xml b/quick_android_webview/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bc94372 --- /dev/null +++ b/quick_android_webview/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/quick_android_webview/src/main/java/cn/qjm253/quick_android_webview/QuickAndroidWebview.kt b/quick_android_webview/src/main/java/cn/qjm253/quick_android_webview/QuickAndroidWebview.kt new file mode 100644 index 0000000..fd88df2 --- /dev/null +++ b/quick_android_webview/src/main/java/cn/qjm253/quick_android_webview/QuickAndroidWebview.kt @@ -0,0 +1,18 @@ +package cn.qjm253.quick_android_webview + +import androidx.fragment.app.FragmentActivity +import cn.qjm253.quick_android_webview.views.QuickAndroidWebview +import cn.qjm253.quick_android_webview.web_bridge.QuickAndroidWebBridge + +/** + * @author SunnyQjm + * @date 19-7-10 下午5:29 + */ +object QuickAndroidWV{ + + fun init(context: FragmentActivity, quickAndroidWebview: QuickAndroidWebview): QuickAndroidWV { + QuickAndroidWebBridge(context) + .bindWebView(quickAndroidWebview) + return this + } +} \ No newline at end of file diff --git a/quick_android_webview/src/main/java/cn/qjm253/quick_android_webview/views/QuickAndroidWebview.kt b/quick_android_webview/src/main/java/cn/qjm253/quick_android_webview/views/QuickAndroidWebview.kt new file mode 100644 index 0000000..f46485d --- /dev/null +++ b/quick_android_webview/src/main/java/cn/qjm253/quick_android_webview/views/QuickAndroidWebview.kt @@ -0,0 +1,180 @@ +package cn.qjm253.quick_android_webview.views + +/** + * @author SunnyQjm + * @date 19-7-7 下午11:24 + */ + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.net.Uri +import android.util.AttributeSet +import androidx.fragment.app.FragmentActivity +import cn.qjm253.quick_android_image_picker.MyImagePicker +import cn.qjm253.quick_android_rx_permission.QuickAndroidRxPermission +import com.qingmei2.rximagepicker.core.RxImagePicker +import com.qingmei2.rximagepicker_extension.MimeType +import com.qingmei2.rximagepicker_extension_wechat.WechatConfigrationBuilder +import com.tencent.smtt.export.external.interfaces.WebResourceError +import com.tencent.smtt.export.external.interfaces.WebResourceRequest +import com.tencent.smtt.sdk.* +import org.jetbrains.anko.selector + +/** + * 腾讯X5浏览器内核封装 + * 1. 支持图片选择 + */ +class QuickAndroidWebview(context: Context, attributeSet: AttributeSet) : + WebView(context, attributeSet) { + + var onWebViewLoadListener: OnWebViewLoadListener? = null + + init { + + val rxPermission = QuickAndroidRxPermission(context as FragmentActivity) + + this.webViewClient = object : WebViewClient() { + + /** + * 防止加载网页时调起系统浏览器 + */ + override fun shouldOverrideUrlLoading(p0: WebView?, p1: String?): Boolean { + p0?.loadUrl(p1 ?: "") + return true + } + + override fun onReceivedError(p0: WebView?, p1: WebResourceRequest?, p2: WebResourceError?) { + super.onReceivedError(p0, p1, p2) + p2?.let { + onWebViewLoadListener?.onLoadFail(p2) + } + } + + override fun onPageFinished(p0: WebView?, p1: String?) { + super.onPageFinished(p0, p1) + onWebViewLoadListener?.onLoadSuccess() + } + } + this.webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(p0: WebView?, p1: Int) { + super.onProgressChanged(p0, p1) + onWebViewLoadListener?.onProgress(p1) + } + + @SuppressLint("CheckResult") + override fun openFileChooser(p0: ValueCallback?, p1: String?, p2: String?) { + rxPermission + .request(arrayOf(Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE)) + .subscribe { + if (it.granted) { + openImageChooserActivity(context, p0) + } + } + } + + @SuppressLint("CheckResult") + override fun onShowFileChooser( + p0: WebView?, + p1: ValueCallback>?, + p2: FileChooserParams? + ): Boolean { + rxPermission + .request(arrayOf(Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE)) + .subscribe { + if (it.granted) { + openImageChooserActivity(context, null, p1) + } + } + return true + } + } + initWebViewSettings() + } + + /** + * 打开图片选择 + */ + private fun openImageChooserActivity( + context: Context, valueCallback: ValueCallback? = null, + valueCallback2: ValueCallback>? = null + ) { + context.selector("选择获取图片方式", listOf("拍照", "相册")) { _, i -> + when (i) { + 0 -> { // 拍照 + // 使用摄像头拍摄 + RxImagePicker.create() + .openCamera(context) + .subscribe { result -> + valueCallback?.onReceiveValue(result.uri) + valueCallback2?.onReceiveValue(arrayOf(result.uri)) + } + } + 1 -> { // 相册选择 + val result = mutableListOf() + // 打开相册选择 + RxImagePicker.create(MyImagePicker::class.java) + .openZhihuStyleGallery( + context, + WechatConfigrationBuilder(MimeType.INSTANCE.ofImage(), false) + .maxSelectable(9) + .countable(true) + .spanCount(4) + .countable(false) + .build() + ) + .subscribe({ res -> + // onNext + result.add(res.uri) + }, { + // onError + + }, { + // onComplete + if (result.size <= 0) { + valueCallback2?.onReceiveValue(null) + } else { + valueCallback2?.onReceiveValue(result.toTypedArray()) + } + }) + + } + } + } + } + + @SuppressLint("SetJavaScriptEnabled") + private fun initWebViewSettings() { + val webSetting = this.settings + webSetting.javaScriptEnabled = true + webSetting.javaScriptCanOpenWindowsAutomatically = true + webSetting.allowFileAccess = true + webSetting.layoutAlgorithm = WebSettings.LayoutAlgorithm.NARROW_COLUMNS + webSetting.setSupportZoom(true) + webSetting.builtInZoomControls = true + webSetting.useWideViewPort = true + webSetting.setSupportMultipleWindows(true) + // webSetting.setLoadWithOverviewMode(true); + webSetting.setAppCacheEnabled(true) + // webSetting.setDatabaseEnabled(true); + webSetting.domStorageEnabled = true + webSetting.setGeolocationEnabled(true) + webSetting.setAppCacheMaxSize(java.lang.Long.MAX_VALUE) + // webSetting.setPageCacheCapacity(IX5WebSettings.DEFAULT_CACHE_CAPACITY); + webSetting.pluginState = WebSettings.PluginState.ON_DEMAND + // webSetting.setRenderPriority(WebSettings.RenderPriority.HIGH); + webSetting.cacheMode = WebSettings.LOAD_NORMAL + + webSetting.setAllowUniversalAccessFromFileURLs(true) + + // this.getSettingsExtension().setPageCacheCapacity(IX5WebSettings.DEFAULT_CACHE_CAPACITY);//extension + // settings 的设计 + } + + + interface OnWebViewLoadListener { + fun onLoadSuccess() + fun onLoadFail(error: WebResourceError) + fun onProgress(progress: Int) + } +} \ No newline at end of file diff --git a/quick_android_webview/src/main/java/cn/qjm253/quick_android_webview/web_bridge/QuickAndroidWebBridge.kt b/quick_android_webview/src/main/java/cn/qjm253/quick_android_webview/web_bridge/QuickAndroidWebBridge.kt new file mode 100644 index 0000000..98ac1c1 --- /dev/null +++ b/quick_android_webview/src/main/java/cn/qjm253/quick_android_webview/web_bridge/QuickAndroidWebBridge.kt @@ -0,0 +1,109 @@ +package cn.qjm253.quick_android_webview.web_bridge + +import android.annotation.SuppressLint +import android.webkit.JavascriptInterface +import androidx.fragment.app.FragmentActivity +import cn.qjm253.quick_android_base.extensions.toJson +import cn.qjm253.quick_android_base.extensions.toast +import cn.qjm253.quick_android_base.util.SystemInfoHelper +import cn.qjm253.quick_android_qrcode.QuickAndroidQrCode +import cn.qjm253.quick_android_webview.views.QuickAndroidWebview +import com.tencent.smtt.sdk.ValueCallback +import java.lang.Exception + +/** + * 与WebView中的Js相互通信的工具类 + * + * @author SunnyQjm + * @date 19-7-8 下午2:48 + */ + +class QuickAndroidWebBridge( + private val context: FragmentActivity +) { + companion object { + const val JS_CALLER_NAME = "QuickAndroidJsCallEr" + + + ////////////////////////////////////////////// + /////// 回调名称定义 + ////////////////////////////////////////////// + const val CALLBACK_BASE_NAME = "Quick_Android_Js_Callback_" + + /** + * 扫码相关回调 + */ + const val CALLBACK_SCAN_CODE_SUCCESS = "${CALLBACK_BASE_NAME}scan_code_success" // 扫码成功回调名 + const val CALLBACK_SCAN_CODE_FAIL = "${CALLBACK_BASE_NAME}scan_code_fail" // 扫码失败回调名 + const val CALLBACK_GET_SYSTEM_INFO_SUCCESS = "${CALLBACK_BASE_NAME}get_system_info_success" // 获取系统信息成功 + const val CALLBACK_GET_SYSTEM_INFO_FAIL = "${CALLBACK_BASE_NAME}get_system_info_fail" // 获取系统信息失败 + } + + private var webView: QuickAndroidWebview? = null + + /** + * 绑定到WebView上 + */ + fun bindWebView(webview: QuickAndroidWebview) { + this.webView = webview + webview.addJavascriptInterface(this, JS_CALLER_NAME) + } + + /** + * 提供给Js调用的显示toast接口 + */ + @JavascriptInterface + fun toast(msg: String) { + context.toast(msg) + } + + + /** + * 提供给Js调用的扫码接口 + */ + @SuppressLint("CheckResult") + @JavascriptInterface + fun scanCode() { + // 调用原生的扫码接口 + webView?.post { + // 需要在主线程中执行 + QuickAndroidQrCode.create(context) + .scanQrCode() + .subscribe({ + // 扫码结果 + callJs(CALLBACK_SCAN_CODE_SUCCESS, it.content) + }, { + // 扫码出错 + callJs(CALLBACK_SCAN_CODE_FAIL, it.message ?: "扫码出错") + }) + } + + } + + /** + * 提供给Js调用,得到系统的信息 + */ + @JavascriptInterface + fun getSystemInfo() { + try { + val systemInfo = SystemInfoHelper + .buildSystemInfo(context) + callJs(CALLBACK_GET_SYSTEM_INFO_SUCCESS, systemInfo.toJson()) + } catch (e: Exception) { + callJs(CALLBACK_GET_SYSTEM_INFO_FAIL, e.message ?: "获取系统信息失败") + } + } + + /** + * 调用Js的回调函数,并且支持传递一个字符串参数 + */ + fun callJs(callbackName: String, param: String = "", callback: ValueCallback = ValueCallback {}) { + webView?.post { + // 执行JS的代码,需要在主线程中进行 + webView?.evaluateJavascript("javascript:$callbackName('$param')", callback) + } + + } +} + + diff --git a/quick_android_webview/src/main/res/values/strings.xml b/quick_android_webview/src/main/res/values/strings.xml new file mode 100644 index 0000000..8542005 --- /dev/null +++ b/quick_android_webview/src/main/res/values/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/quick_android_webview/src/test/java/cn/qjm253/quick_android_webview/ExampleUnitTest.java b/quick_android_webview/src/test/java/cn/qjm253/quick_android_webview/ExampleUnitTest.java new file mode 100644 index 0000000..862f1a3 --- /dev/null +++ b/quick_android_webview/src/test/java/cn/qjm253/quick_android_webview/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package cn.qjm253.quick_android_webview; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 38427ab..2e38dff 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,11 @@ rootProject.name = "MIN-VPN-Client_V2" include ':app' +include ':quick_android_base' +include ':quick_android_custom_view' +include ':quick_android_easy_bar' +include ':quick_android_file_picker' +include ':quick_android_image_picker' +include ':quick_android_mvp' +include ':quick_android_qrcode' +include ':quick_android_rx_permission' +include ':quick_android_webview'