diff --git a/background/build.gradle.kts b/background/build.gradle.kts index 06240ee..c381938 100644 --- a/background/build.gradle.kts +++ b/background/build.gradle.kts @@ -20,12 +20,21 @@ android { } buildTypes { + val appName = "${rootProject.extra["background_app_name"]}" + debug { + manifestPlaceholders.apply { + this["background_app_name"] = appName + } + } release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + manifestPlaceholders.apply { + this["background_app_name"] = appName + } } } compileOptions { @@ -34,29 +43,32 @@ android { } kotlinOptions { jvmTarget = "1.8" - useIR = true } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = rootProject.extra["compose_version"] as String - kotlinCompilerVersion = "1.4.32" } } dependencies { + implementation(project(":lib")) - implementation("androidx.core:core-ktx:1.3.2") - implementation("androidx.appcompat:appcompat:1.2.0") - implementation("com.google.android.material:material:1.3.0") - implementation("androidx.compose.ui:ui:${rootProject.extra["compose_version"]}") - implementation("androidx.compose.material:material:${rootProject.extra["compose_version"]}") - implementation("androidx.compose.ui:ui-tooling:${rootProject.extra["compose_version"]}") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") - implementation("androidx.activity:activity-compose:1.3.0-alpha07") + // optional - Test helpers + testImplementation("androidx.room:room-testing:${rootProject.extra["room_version"]}") + //测试 testImplementation("junit:junit:4.13.2") + /** + * A cross environment JUnit4 runner for Android tests. + * https://developer.android.com/reference/androidx/test/ext/junit/runners/package-summary?hl=en + */ androidTestImplementation("androidx.test.ext:junit:1.1.2") + /** + * https://developer.android.com/training/testing/espresso + * 使用 Espresso 来编写简洁、美观且可靠的 Android 界面测试。 + * 包含核心和基本的 View 匹配器、操作和断言 + */ androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0") androidTestImplementation("androidx.compose.ui:ui-test-junit4:${rootProject.extra["compose_version"]}") } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index c697028..edde590 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { //Jetpack Compose版本 - val compose_version by extra("1.0.0-beta05") + val compose_version by extra("1.0.0-beta06") //生命周期组件版本 val lifecycle_version by extra("2.3.1") //APP应用名字 @@ -13,6 +13,8 @@ buildscript { repositories { maven("https://maven.aliyun.com/repository/google") maven("https://maven.aliyun.com/repository/public") + google() + mavenCentral() } dependencies { classpath("com.android.tools.build:gradle:7.0.0-alpha15") diff --git a/foreground/build.gradle.kts b/foreground/build.gradle.kts index 2b19d69..2f2a050 100644 --- a/foreground/build.gradle.kts +++ b/foreground/build.gradle.kts @@ -1,7 +1,6 @@ plugins { id("com.android.application") id("kotlin-android") - id("org.jetbrains.kotlin.plugin.serialization") version "1.4.32" id("kotlin-kapt") } @@ -50,7 +49,6 @@ android { } kotlinOptions { jvmTarget = "1.8" - useIR = true } buildFeatures { compose = true @@ -65,76 +63,9 @@ android { } dependencies { - /** - * 针对最新的平台功能和 API 调整应用,同时还支持旧设备。 - * https://developer.android.com/jetpack/androidx/releases/core - */ - implementation("androidx.core:core-ktx:1.3.2") - /** - * 允许在平台旧版 API 上访问新 API(很多使用 Material Design)。 - * https://developer.android.com/jetpack/androidx/releases/appcompat - */ - implementation("androidx.appcompat:appcompat:1.2.0") - /** - * 与设备互动所需的 Compose UI 的基本组件,包括布局、绘图和输入。 - * https://developer.android.com/jetpack/androidx/releases/compose-ui - */ - implementation("androidx.compose.ui:ui:${rootProject.extra["compose_version"]}") - implementation("androidx.compose.ui:ui-tooling:${rootProject.extra["compose_version"]}") - /** - * Compose 的编程模型和状态管理的基本构建块,以及 Compose 编译器插件针对的核心运行时。 - * https://developer.android.com/jetpack/androidx/releases/compose-runtime - */ - implementation("androidx.compose.runtime:runtime-livedata:${rootProject.extra["compose_version"]}") - //Material Components - implementation("com.google.android.material:material:1.3.0") - /** - * 使用现成可用的 Material Design 组件构建 Jetpack Compose UI。这是更高层级的 Compose 入口点,旨在提供与 www.material.io 上描述的组件一致的组件。 - * https://developer.android.com/jetpack/androidx/releases/compose-material - */ - implementation("androidx.compose.material:material:${rootProject.extra["compose_version"]}") - /** - * 生命周期感知型组件 - * https://developer.android.com/jetpack/androidx/releases/lifecycle - */ - implementation("androidx.lifecycle:lifecycle-runtime-ktx:${rootProject.extra["lifecycle_version"]}") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha04") - /** - * 访问基于 Activity 构建的可组合 API。 - * https://developer.android.com/jetpack/androidx/releases/activity - */ - implementation("androidx.activity:activity-compose:1.3.0-alpha07") - /** - * Simple, pretty and powerful logger for android - * https://github.com/orhanobut/logger - */ - implementation("com.orhanobut:logger:2.2.0") - /** - * - * https://github.com/square/okhttp - */ - implementation("com.squareup.okhttp3:okhttp:4.9.1") - /** - * https://github.com/google/gson - */ - implementation("com.google.code.gson:gson:2.8.6") - /** - * https://kotlinlang.org/docs/serialization.html - */ - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0") - - implementation("org.jetbrains.kotlin:kotlin-reflect:${rootProject.extra["kotlin_version"]}") - /** - * https://developer.android.com/jetpack/androidx/releases/navigation - */ - implementation("androidx.navigation:navigation-compose:1.0.0-alpha10") - /** - * https://developer.android.com/jetpack/androidx/releases/room - */ - implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}") + implementation(project(":lib")) kapt("androidx.room:room-compiler:${rootProject.extra["room_version"]}") - // optional - Kotlin Extensions and Coroutines support for Room - implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}") + // optional - Test helpers testImplementation("androidx.room:room-testing:${rootProject.extra["room_version"]}") //测试 @@ -152,5 +83,4 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0") androidTestImplementation("androidx.compose.ui:ui-test-junit4:${rootProject.extra["compose_version"]}") - } \ No newline at end of file diff --git a/foreground/src/main/java/com/gyf/csams/APP.kt b/foreground/src/main/java/com/gyf/csams/APP.kt index aa3fe94..ef19edf 100644 --- a/foreground/src/main/java/com/gyf/csams/APP.kt +++ b/foreground/src/main/java/com/gyf/csams/APP.kt @@ -1,112 +1,75 @@ package com.gyf.csams import android.app.Application -import android.content.res.Resources -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.util.LruCache import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap +import coil.ImageLoader +import coil.ImageLoaderFactory +import coil.imageLoader +import coil.memory.MemoryCache +import coil.request.ImageRequest +import coil.util.CoilUtils import com.gyf.csams.uikit.BackgroundImage +import com.gyf.csams.util.ImageUtil import com.orhanobut.logger.AndroidLogAdapter import com.orhanobut.logger.DiskLogAdapter import com.orhanobut.logger.Logger +import okhttp3.OkHttpClient -class APP : Application() { +class APP : Application(), ImageLoaderFactory { + private val backgroundImage = mutableMapOf() - private lateinit var memoryCache: LruCache - - // Get max available VM memory, exceeding this amount will throw an - // OutOfMemory exception. Stored in kilobytes as LruCache takes an - // int in its constructor. - private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() - - // Use 1/8th of the available memory for this memory cache. - val cacheSize = maxMemory / 8 - - fun getImage(image: BackgroundImage, reqWidth: Int, reqHeight: Int): ImageBitmap { - val bitmap = memoryCache.get(image) - return if (bitmap == null) { - Logger.i("reqWidth=$reqWidth,reqHeight=$reqHeight") - val cacheValue = decodeSampledBitmapFromResource( - res = resources, - image.id, - reqWidth = reqWidth, - reqHeight = reqHeight - ) - memoryCache.put(image, cacheValue) - Logger.i("添加缓存:${image}") - cacheValue.asImageBitmap() + suspend fun getImage(image: BackgroundImage): ImageBitmap? { + val key = backgroundImage[image] + if (key != null) { + return applicationContext.imageLoader.memoryCache[key]?.asImageBitmap() + ?: ImageUtil.getImage(applicationContext, image.id) } else { - Logger.i("从缓存读取:${image}") - bitmap.asImageBitmap() + throw IllegalArgumentException("无法从${key}获取背景图!") } } - private fun decodeSampledBitmapFromResource( - res: Resources, - resId: Int, - reqWidth: Int, - reqHeight: Int - ): Bitmap { - // First decode with inJustDecodeBounds=true to check dimensions - return BitmapFactory.Options().run { - inJustDecodeBounds = true - BitmapFactory.decodeResource(res, resId, this) - - // Calculate inSampleSize - inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) - - // Decode bitmap with inSampleSize set - inJustDecodeBounds = false - - BitmapFactory.decodeResource(res, resId, this) - } + override fun newImageLoader(): ImageLoader { + return ImageLoader.Builder(applicationContext) + .crossfade(true) + .okHttpClient { + OkHttpClient.Builder() + .cache(CoilUtils.createDefaultCache(applicationContext)) + .build() + } + .build() } - private fun calculateInSampleSize( - options: BitmapFactory.Options, - reqWidth: Int, - reqHeight: Int - ): Int { - // Raw height and width of image - val (height: Int, width: Int) = options.run { outHeight to outWidth } - var inSampleSize = 1 - - if (height > reqHeight || width > reqWidth) { - - val halfHeight: Int = height / 2 - val halfWidth: Int = width / 2 - - // Calculate the largest inSampleSize value that is a power of 2 and keeps both - // height and width larger than the requested height and width. - while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { - inSampleSize *= 2 - } + /** + * 预加载背景图 + * + */ + private fun preloadImage() { + BackgroundImage.values().forEach { + Logger.i("预加载背景图:${it.name}") + val request = ImageRequest.Builder(applicationContext) + .data(it.id) + // Optional, but setting a ViewSizeResolver will conserve memory by limiting the size the image should be preloaded into memory at. + .size(800, 600) + .listener { _, metadata -> + Logger.i("metadata.memoryCacheKey=${metadata.memoryCacheKey}") + backgroundImage[it] = metadata.memoryCacheKey + } + .build() + applicationContext.imageLoader.enqueue(request = request) } - return inSampleSize } override fun onCreate() { super.onCreate() - - memoryCache = object : LruCache(cacheSize) { - - override fun sizeOf(key: BackgroundImage, bitmap: Bitmap): Int { - // The cache size will be measured in kilobytes rather than - // number of items. - return bitmap.byteCount / 1024 - } - } - //初始化日志 Logger.addLogAdapter(AndroidLogAdapter()) Logger.addLogAdapter(DiskLogAdapter()) Logger.i("${BuildConfig.foreground_app_name}启动") - + preloadImage() } } \ No newline at end of file diff --git a/foreground/src/main/java/com/gyf/csams/Api.kt b/foreground/src/main/java/com/gyf/csams/Api.kt index 59a431e..5c82c01 100644 --- a/foreground/src/main/java/com/gyf/csams/Api.kt +++ b/foreground/src/main/java/com/gyf/csams/Api.kt @@ -11,10 +11,10 @@ interface UrlPath { * @property path */ enum class AccountApi(val path: String) : UrlPath { - register("/register"), - checkId("/register/checkId"), - login("/login"), - loginToken("/login/token"); + Register("/register"), + CheckId("/register/checkId"), + Login("/login"), + LoginToken("/login/token"); override fun build(): String { @@ -22,6 +22,14 @@ enum class AccountApi(val path: String) : UrlPath { } } +enum class MainApi(val path: String) : UrlPath { + HotActivity("/hotActivity"); + + override fun build(): String { + return "/api/main/${this.path}" + } +} + /** * 构建服务端请求接口地址 * diff --git a/foreground/src/main/java/com/gyf/csams/InitViewModel.kt b/foreground/src/main/java/com/gyf/csams/InitViewModel.kt index 37992ca..0944146 100644 --- a/foreground/src/main/java/com/gyf/csams/InitViewModel.kt +++ b/foreground/src/main/java/com/gyf/csams/InitViewModel.kt @@ -41,7 +41,7 @@ class InitViewModel : ViewModel() { val tokenList = db?.tokenDao()?.queryAll() if (tokenList != null && tokenList.size == 1) { val currentToken: Token = tokenList[0] - val url = Api.buildUrl(AccountApi.loginToken) + val url = Api.buildUrl(AccountApi.LoginToken) val action = "校验token" Logger.i("${action}api=$url") HttpClient.post( diff --git a/foreground/src/main/java/com/gyf/csams/account/model/AccountViewModel.kt b/foreground/src/main/java/com/gyf/csams/account/model/AccountViewModel.kt index c1a1a99..2fdbae3 100644 --- a/foreground/src/main/java/com/gyf/csams/account/model/AccountViewModel.kt +++ b/foreground/src/main/java/com/gyf/csams/account/model/AccountViewModel.kt @@ -176,7 +176,7 @@ class AccountViewModel(application: Application) : AndroidViewModel(application) } else { _isRepeat.postValue(null) checkJob = viewModelScope.launch { - val url = Api.buildUrl(AccountApi.checkId) + val url = Api.buildUrl(AccountApi.CheckId) Logger.i("检测${studentId.formDesc},请求接口$url") HttpClient.get( url, SimpleCallback( @@ -232,7 +232,7 @@ class AccountViewModel(application: Application) : AndroidViewModel(application) */ fun register() { if (checkForm()) { - val url = Api.buildUrl(AccountApi.register) + val url = Api.buildUrl(AccountApi.Register) Logger.i("开始$regBtnDesc,请求接口:$url") HttpClient.post( url, SimpleCallback( @@ -288,7 +288,7 @@ class AccountViewModel(application: Application) : AndroidViewModel(application) */ fun login() { if (checkForm()) { - val url = Api.buildUrl(AccountApi.login) + val url = Api.buildUrl(AccountApi.Login) Logger.i("开始$loginDesc,请求接口:$url") HttpClient.post( url, diff --git a/foreground/src/main/java/com/gyf/csams/association/ui/AssociationActivity.kt b/foreground/src/main/java/com/gyf/csams/association/ui/AssociationActivity.kt index 101aa2a..dd64bb6 100644 --- a/foreground/src/main/java/com/gyf/csams/association/ui/AssociationActivity.kt +++ b/foreground/src/main/java/com/gyf/csams/association/ui/AssociationActivity.kt @@ -432,7 +432,7 @@ class AssociationActivity : ComponentActivity() { val weight = 0.5F val spaceWeight = (1 - 0.5F) / 2 Spacer(modifier = Modifier.weight(spaceWeight)) - Poster(id = R.drawable.ic_launcher_foreground, modifier = Modifier.weight(weight)) + Poster(modifier = Modifier.weight(weight)) Spacer(modifier = Modifier.weight(spaceWeight)) } diff --git a/foreground/src/main/java/com/gyf/csams/main/model/MainViewModel.kt b/foreground/src/main/java/com/gyf/csams/main/model/MainViewModel.kt index ebe4739..3ee6b01 100644 --- a/foreground/src/main/java/com/gyf/csams/main/model/MainViewModel.kt +++ b/foreground/src/main/java/com/gyf/csams/main/model/MainViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.gyf.csams.NOT_IMPL_TIP -import com.gyf.csams.R import com.gyf.csams.uikit.ScrollList import com.gyf.csams.uikit.SendInterface import com.gyf.csams.uikit.StringForm @@ -43,39 +42,6 @@ class MarqueeViewModel : ViewModel() { } } -/** - * 海报轮播 - * - */ -class CarouselViewModel : ViewModel() { - val imageList = listOf( - R.drawable.ic_launcher_foreground, - R.drawable.ic_account_fill, - R.drawable.ic_all_fill, - R.drawable.ic_home_fill - ) - - private val _index = MutableLiveData(0) - - val index: LiveData = _index - - private var job: Job? = null - - init { - start() - } - - private fun start() { - job = viewModelScope.launch { - do { - _index.postValue(if (_index.value == imageList.size - 1) 0 else _index.value?.plus(1)) - delay(5000) - } while (job?.isActive == true) - } - } - -} - /** * 社团 * diff --git a/foreground/src/main/java/com/gyf/csams/main/ui/MainActivity.kt b/foreground/src/main/java/com/gyf/csams/main/ui/MainActivity.kt index faffe03..7ced8a3 100644 --- a/foreground/src/main/java/com/gyf/csams/main/ui/MainActivity.kt +++ b/foreground/src/main/java/com/gyf/csams/main/ui/MainActivity.kt @@ -25,6 +25,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import com.gyf.csams.Api +import com.gyf.csams.MainApi import com.gyf.csams.R import com.gyf.csams.activity.ui.ActivityDetailActivity import com.gyf.csams.association.ui.AssociationActivity @@ -41,9 +43,14 @@ import com.gyf.csams.util.randomChinese * */ class MainActivity : ComponentActivity() { + + lateinit var imageModel: ImageModel + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + imageModel = ImageModel(application = application, Api.buildUrl(MainApi.HotActivity)) + setContent { CSAMSTheme { Body { nav, scaffoldState -> @@ -69,6 +76,22 @@ class MainActivity : ComponentActivity() { } + override fun onStart() { + super.onStart() + imageModel.start() + } + + override fun onResume() { + super.onResume() + imageModel.start() + } + + override fun onPause() { + super.onPause() + imageModel.cancel() + } + + /** * 个人中心 * @@ -467,16 +490,17 @@ class MainActivity : ComponentActivity() { * */ @Composable - private fun PosterWithDesc(model: CarouselViewModel = viewModel()) { - Carousel(model = model) { - val context = LocalContext.current + private fun PosterWithDesc() { + + val context = LocalContext.current as MainActivity + Carousel(imageBitmap = context.imageModel.image) { Column(modifier = Modifier.clickable(onClick = { context.startActivity(Intent(context, ActivityDetailActivity::class.java)) })) { Poster( modifier = Modifier .weight(0.6F) - .fillMaxWidth(), id = it + .fillMaxWidth() ) DescCard( diff --git a/foreground/src/main/java/com/gyf/csams/uikit/BaseView.kt b/foreground/src/main/java/com/gyf/csams/uikit/BaseView.kt index 2d46753..fb5ae49 100644 --- a/foreground/src/main/java/com/gyf/csams/uikit/BaseView.kt +++ b/foreground/src/main/java/com/gyf/csams/uikit/BaseView.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.DefaultAlpha +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -34,7 +35,6 @@ import androidx.navigation.compose.navigate import androidx.navigation.compose.rememberNavController import com.gyf.csams.APP import com.gyf.csams.R -import com.gyf.csams.main.model.CarouselViewModel import com.gyf.csams.main.model.MarqueeViewModel import com.orhanobut.logger.Logger import kotlinx.coroutines.launch @@ -462,13 +462,14 @@ fun MainFrame(background: @Composable () -> Unit, body: @Composable ColumnScope. */ @Composable fun Carousel( - model: CarouselViewModel = viewModel(), + imageBitmap: LiveData, durationMillis: Int = 2000, - content: @Composable (id: Int) -> Unit + content: @Composable (imageBitmap: ImageBitmap?) -> Unit ) { - val index: Int by model.index.observeAsState(0) - Crossfade(targetState = index, animationSpec = tween(durationMillis = durationMillis)) { - content(id = model.imageList[it]) + val data by imageBitmap.observeAsState() + + Crossfade(targetState = data, animationSpec = tween(durationMillis = durationMillis)) { + content(imageBitmap = it) } } @@ -600,20 +601,22 @@ enum class BackgroundImage(@DrawableRes val id: Int) { @Composable fun Background(image: BackgroundImage, alpha: Float = DefaultAlpha) { val app = LocalContext.current.applicationContext as APP - - BoxWithConstraints { + var i: ImageBitmap? by remember { + mutableStateOf(null) + } + LaunchedEffect(image) { + i = app.getImage(image = image) + } + i?.let { Image( - bitmap = app.getImage( - image = image, - reqHeight = maxHeight.value.toInt() / 2, - reqWidth = maxWidth.value.toInt() / 2 - ), + bitmap = it, contentDescription = null, - contentScale = ContentScale.FillHeight, + contentScale = ContentScale.FillBounds, alpha = alpha, modifier = Modifier.fillMaxSize() ) } + } /** @@ -654,7 +657,7 @@ fun Body(content: @Composable (nav: NavHostController, scaffoldState: ScaffoldSt * */ @Composable -fun Poster(modifier: Modifier = Modifier, @DrawableRes id: Int) { +fun Poster(modifier: Modifier = Modifier, imageBitmap: ImageBitmap? = null) { Card( modifier = modifier, backgroundColor = Color.Transparent @@ -665,11 +668,18 @@ fun Poster(modifier: Modifier = Modifier, @DrawableRes id: Int) { contentDescription = null, modifier = Modifier.fillMaxSize() ) - Image( - painter = painterResource(id = id), - contentDescription = null, - modifier = Modifier.fillMaxSize() - ) + if (imageBitmap != null) { + Image( + bitmap = imageBitmap, + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } else { + Image( + painter = painterResource(id = R.drawable.ic_launcher_foreground), + contentDescription = null + ) + } } } } @@ -764,4 +774,8 @@ fun MyBottomAppBarPreview() { } } } +} + +fun TestPreview() { + } \ No newline at end of file diff --git a/foreground/src/main/java/com/gyf/csams/uikit/ViewModel.kt b/foreground/src/main/java/com/gyf/csams/uikit/ViewModel.kt index 985563c..9bb5e00 100644 --- a/foreground/src/main/java/com/gyf/csams/uikit/ViewModel.kt +++ b/foreground/src/main/java/com/gyf/csams/uikit/ViewModel.kt @@ -1,10 +1,18 @@ package com.gyf.csams.uikit +import android.app.Application import androidx.compose.material.SnackbarDuration -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel +import androidx.compose.ui.graphics.ImageBitmap +import androidx.lifecycle.* +import com.google.gson.reflect.TypeToken +import com.gyf.csams.R +import com.gyf.csams.util.HttpClient +import com.gyf.csams.util.ImageUtil +import com.gyf.csams.util.SimpleCallback import com.orhanobut.logger.Logger +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch interface FormLength { val nameLengthError: String @@ -77,6 +85,74 @@ class ScaffoldModel : ViewModel() { } } + +class ImageModel(application: Application, private val urlPath: String) : + AndroidViewModel(application) { + private val _image = MutableLiveData() + private val _imageUrls = MutableLiveData>() + val image: LiveData = _image + private var job: Job? = null + + /** + * 加载默认图片 + * + */ + private fun defaultLoad() { + viewModelScope.launch { + _image.postValue( + ImageUtil.getImage( + getApplication(), + R.drawable.ic_launcher_foreground + ) + ) + } + } + + /** + * 循环加载网络图片 + * + */ + fun start() { + Logger.i("启动轮播") + if (job == null || job?.isCompleted == true || job?.isCancelled == true) { + job = viewModelScope.launch { + HttpClient.get( + url = urlPath, + SimpleCallback>("请求图片url列表", onSuccess = { + _imageUrls.postValue(it.body) + var index = 0 + _imageUrls.value?.apply { + viewModelScope.launch { + do { + val imageBitmap = + ImageUtil.getImage( + context = getApplication(), + data = get(index) + ) + Logger.e("成功从image url:${get(index)}解析图片") + _image.postValue(imageBitmap) + delay(5000) + index = if (index == size - 1) 0 else index.plus(1) + } while (job?.isActive == true) + } + } + }, onFail = { + Logger.e("无法从接口地址:${urlPath}获取图片url列表") + defaultLoad() + }, type = object : TypeToken>() {}.type) + ) + }.apply { + start() + } + } + } + + fun cancel() { + Logger.i("停止轮播") + job?.cancel() + } +} + abstract class ScrollList : ViewModel() { protected val _data = MutableLiveData>(mutableListOf()) val data: LiveData> = _data diff --git a/foreground/src/main/java/com/gyf/csams/util/HttpUtil.kt b/foreground/src/main/java/com/gyf/csams/util/HttpUtil.kt index 93ccbc5..8488d5b 100644 --- a/foreground/src/main/java/com/gyf/csams/util/HttpUtil.kt +++ b/foreground/src/main/java/com/gyf/csams/util/HttpUtil.kt @@ -15,7 +15,12 @@ object HttpClient { private val JSON_CONTENT_TYPE = "application/json; charset=UTF-8".toMediaType() - + /** + * 构建url查询参数 + * + * @param params + * @return + */ private fun buildQueryParams(params: Map?): String { return if (params?.isNotEmpty() == true) { val urlPath = StringBuilder("?") @@ -29,6 +34,12 @@ object HttpClient { } } + /** + * 构建表单参数 + * + * @param params + * @return + */ private fun buildFormBody(params: Map?): FormBody { val builder = FormBody.Builder() if (params?.isNotEmpty() == true) { @@ -92,10 +103,25 @@ object HttpClient { } - +/** + * 接口响应实体 + * + * @param T + * @property code + * @property message + * @property body + */ data class ApiResponse(val code: Int, val message: String, val body: T? = null) - +/** + * http请求回调 + * + * @param T + * @property action + * @property onSuccess + * @property onFail + * @property type + */ class SimpleCallback( private val action: String, private val onSuccess: (res: ApiResponse) -> Unit, diff --git a/foreground/src/main/java/com/gyf/csams/util/ImageUtil.kt b/foreground/src/main/java/com/gyf/csams/util/ImageUtil.kt new file mode 100644 index 0000000..f0dcb26 --- /dev/null +++ b/foreground/src/main/java/com/gyf/csams/util/ImageUtil.kt @@ -0,0 +1,46 @@ +package com.gyf.csams.util + +import android.content.Context +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.core.graphics.drawable.toBitmap +import coil.imageLoader +import coil.request.ErrorResult +import coil.request.ImageRequest +import coil.request.SuccessResult +import com.orhanobut.logger.Logger + +class ImageUtil { + companion object { + /** + * + * + * @param data Set the data to load. + * The default supported data types are: + * String (mapped to a Uri) + * Uri ("android.resource", "content", "file", "http", and "https" schemes only) + * HttpUrl + * File + * DrawableRes + * Drawable + * Bitmap + */ + suspend fun getImage(context: Context, data: Any?): ImageBitmap? { + val request = ImageRequest.Builder(context) + .data(data = data) + .build() + return when (val result = context.imageLoader.execute(request)) { + is SuccessResult -> result.drawable.toBitmap().asImageBitmap() + is ErrorResult -> { + val drawable = result.drawable + return if (drawable == null) { + Logger.e("${data}图片加载失败") + null + } else { + drawable.toBitmap().asImageBitmap() + } + } + } + } + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 573d8fa..0f80bbf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Sat Apr 17 15:25:36 CST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 153015f..4f906e0 100644 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -24,11 +40,11 @@ cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null -foreground_app_name="Gradle" +APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -66,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -106,13 +123,14 @@ fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$foreground_app_name\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -138,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f955316..107acd3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts new file mode 100644 index 0000000..88648cc --- /dev/null +++ b/lib/build.gradle.kts @@ -0,0 +1,106 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("org.jetbrains.kotlin.plugin.serialization") version "1.4.32" +} + +android { + compileSdk = 30 + + defaultConfig { + minSdk = 21 + targetSdk = 30 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + /** + * 针对最新的平台功能和 API 调整应用,同时还支持旧设备。 + * https://developer.android.com/jetpack/androidx/releases/core + */ + api("androidx.core:core-ktx:1.3.2") + /** + * 允许在平台旧版 API 上访问新 API(很多使用 Material Design)。 + * https://developer.android.com/jetpack/androidx/releases/appcompat + */ + api("androidx.appcompat:appcompat:1.2.0") + /** + * 与设备互动所需的 Compose UI 的基本组件,包括布局、绘图和输入。 + * https://developer.android.com/jetpack/androidx/releases/compose-ui + */ + api("androidx.compose.ui:ui:${rootProject.extra["compose_version"]}") + api("androidx.compose.ui:ui-tooling:${rootProject.extra["compose_version"]}") + /** + * Compose 的编程模型和状态管理的基本构建块,以及 Compose 编译器插件针对的核心运行时。 + * https://developer.android.com/jetpack/androidx/releases/compose-runtime + */ + api("androidx.compose.runtime:runtime-livedata:${rootProject.extra["compose_version"]}") + //Material Components + api("com.google.android.material:material:1.3.0") + /** + * 使用现成可用的 Material Design 组件构建 Jetpack Compose UI。这是更高层级的 Compose 入口点,旨在提供与 www.material.io 上描述的组件一致的组件。 + * https://developer.android.com/jetpack/androidx/releases/compose-material + */ + api("androidx.compose.material:material:${rootProject.extra["compose_version"]}") + /** + * 生命周期感知型组件 + * https://developer.android.com/jetpack/androidx/releases/lifecycle + */ + api("androidx.lifecycle:lifecycle-runtime-ktx:${rootProject.extra["lifecycle_version"]}") + api("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha04") + /** + * 访问基于 Activity 构建的可组合 API。 + * https://developer.android.com/jetpack/androidx/releases/activity + */ + api("androidx.activity:activity-compose:1.3.0-alpha07") + /** + * Simple, pretty and powerful logger for android + * https://github.com/orhanobut/logger + */ + api("com.orhanobut:logger:2.2.0") + /** + * + * https://github.com/square/okhttp + */ + api("com.squareup.okhttp3:okhttp:4.9.1") + /** + * https://github.com/google/gson + */ + api("com.google.code.gson:gson:2.8.6") + /** + * https://kotlinlang.org/docs/serialization.html + */ + api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0") + /** + * https://developer.android.com/jetpack/androidx/releases/navigation + */ + api("androidx.navigation:navigation-compose:1.0.0-alpha10") + /** + * https://developer.android.com/jetpack/androidx/releases/room + */ + api("androidx.room:room-runtime:${rootProject.extra["room_version"]}") + // optional - Kotlin Extensions and Coroutines support for Room + api("androidx.room:room-ktx:${rootProject.extra["room_version"]}") + // https://github.com/coil-kt/coil/blob/master/README-zh.md + api("io.coil-kt:coil:1.2.1") +} \ No newline at end of file diff --git a/lib/consumer-rules.pro b/lib/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/lib/proguard-rules.pro b/lib/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/lib/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 \ No newline at end of file diff --git a/lib/src/androidTest/java/com/gyf/lib/ExampleInstrumentedTest.kt b/lib/src/androidTest/java/com/gyf/lib/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..0b377f0 --- /dev/null +++ b/lib/src/androidTest/java/com/gyf/lib/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.gyf.lib + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith + +/** + * 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.gyf.lib.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4bfa72b --- /dev/null +++ b/lib/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lib/src/test/java/com/gyf/lib/ExampleUnitTest.kt b/lib/src/test/java/com/gyf/lib/ExampleUnitTest.kt new file mode 100644 index 0000000..ae27851 --- /dev/null +++ b/lib/src/test/java/com/gyf/lib/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.gyf.lib + +import org.junit.Assert.* +import org.junit.Test + +/** + * 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) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c857320..dc9ec1e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,8 +4,14 @@ dependencyResolutionManagement { maven("https://maven.aliyun.com/repository/google") maven("https://maven.aliyun.com/repository/public") maven("https://maven.aliyun.com/repository/jcenter") + google() + mavenCentral() } } rootProject.name = "CSAMS" +//前台 include(":foreground") +//后台 include(":background") +//公共库 +include(":lib")