增加社团注册

优化背景加载
通用文本框、底部提示封装
master
pan 4 years ago
parent 3bcab0cb2e
commit 014d4ae16c
  1. 192
      app/src/androidTest/java/com/gyf/csams/TestPreview.kt
  2. 10
      app/src/main/AndroidManifest.xml
  3. 81
      app/src/main/java/com/gyf/csams/APP.kt
  4. 6
      app/src/main/java/com/gyf/csams/InitActivity.kt
  5. 11
      app/src/main/java/com/gyf/csams/account/ui/AccountActivity.kt
  6. 45
      app/src/main/java/com/gyf/csams/association/model/RegAssociationViewModel.kt
  7. 258
      app/src/main/java/com/gyf/csams/association/ui/RegAssociationActivity.kt
  8. 110
      app/src/main/java/com/gyf/csams/main/model/MainViewModel.kt
  9. 188
      app/src/main/java/com/gyf/csams/main/ui/MainActivity.kt
  10. 146
      app/src/main/java/com/gyf/csams/uikit/BaseView.kt
  11. 41
      app/src/main/java/com/gyf/csams/uikit/ViewModel.kt
  12. 2
      app/src/main/java/com/gyf/csams/uikit/theme/Color.kt
  13. 2
      app/src/main/java/com/gyf/csams/uikit/theme/Shape.kt
  14. 2
      app/src/main/java/com/gyf/csams/uikit/theme/Theme.kt
  15. 2
      app/src/main/java/com/gyf/csams/uikit/theme/Type.kt
  16. 9
      app/src/main/res/drawable/ic_exchange_rate.xml
  17. 7
      app/src/test/java/com/gyf/csams/ExampleUnitTest.kt

@ -0,0 +1,192 @@
package com.gyf.csams
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun MyBox(modifier: Modifier, canvasSize:Dp, canvasD:Float, content: @Composable BoxScope.() -> Unit){
Box(modifier = modifier,contentAlignment = Alignment.Center){
Canvas(modifier = Modifier.size(canvasSize)) {
rotate(canvasD){
drawRect(color = Color.Cyan)
}
}
content()
}
}
@Composable
fun BoxSetSize(degrees:Float=0F, content: @Composable BoxScope.() -> Unit){
MyBox(modifier = Modifier
.height(300.dp)
.fillMaxWidth()
.background(Color.Gray)
.rotate(degrees = degrees), canvasSize = 200.dp, canvasD = 45F,content = content
)
}
@Composable
fun BoxFillSize(degrees:Float=0F, content: @Composable BoxScope.() -> Unit){
MyBox(
modifier = Modifier
.fillMaxSize()
.background(Color.LightGray), canvasSize = 100.dp, canvasD = 80F,content = content
)
}
//@Preview
@Composable
fun TestPreview(){
Column(modifier = Modifier.fillMaxSize()){
BoxSetSize {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("前置子布局固定尺寸")
}
})
}
BoxFillSize {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("后置子布局使用")
}
withStyle(style = SpanStyle(color= Color.Red,fontSize = 30.sp)){
append("fillMaxSize修饰符")
}
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("填充父项允许的所有可用空间")
}
})
}
}
}
//@Preview
@Composable
fun TestPreview2(){
Column(modifier = Modifier.fillMaxSize()){
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("如果使用示例1方法是")
}
withStyle(style = SpanStyle(color= Color.Red,fontSize = 30.sp)){
append("无法实现前置子布局填充父项所有可用空间,后置子布局固定尺寸")
}
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("因为前置布局已经填充父项允许的所有可用空间,后置子布局没有剩余空间可用")
}
})
BoxFillSize {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("前置子布局填充父项允许的所有可用空间")
}
})
}
BoxSetSize {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("后置子布局固定尺寸")
}
})
}
}
}
//@Preview
@Composable
fun TestPreview3(){
Column(modifier = Modifier
.fillMaxSize()) {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("折衷方案是通过父项旋转180°实现,子项分别旋转180°复位可")
}
withStyle(style = SpanStyle(color= Color.Red,fontSize = 30.sp)){
append("实现前置子布局填充父项所有可用空间,后置子布局固定尺寸")
}
withStyle(style = SpanStyle(fontSize = 30.sp)){
append(",除了旋转,应该有更好的实现方式?")
}
})
Column(modifier = Modifier.rotate(180F)){
BoxSetSize(degrees = 180F) {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("前置子布局固定尺寸,通过旋转和后置子布局对调位置")
}
})
}
BoxFillSize(degrees = 180F) {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("后置子布局使用")
}
withStyle(style = SpanStyle(color= Color.Red,fontSize = 30.sp)){
append("fillMaxSize修饰符")
}
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("填充父项允许的所有可用空间,通过旋转和前置子布局对调位置")
}
})
}
}
}
}
//@Preview
@Composable
fun TestPreview4(){
Column(modifier = Modifier
.fillMaxSize()) {
MyBox(modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(Color.Gray), canvasSize = 100.dp, canvasD = 80F){
Text(text = "Box1")
}
MyBox(modifier = Modifier
.height(300.dp)
.fillMaxWidth()
.background(Color.Gray), canvasSize = 200.dp, canvasD = 45F){
Text(text = "Box2")
}
}
}
@Preview
@Composable
fun TestPreview5(){
Column {
Row {
}
}
}

@ -3,6 +3,10 @@
package="com.gyf.csams">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!--已启用分区存储-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<application
android:allowBackup="true"
@ -33,7 +37,11 @@
</activity>
<activity
android:name=".ui.MainActivity"
android:name=".main.ui.MainActivity"
android:exported="true">
</activity>
<activity android:name=".association.ui.RegAssociationActivity"
android:exported="true">
</activity>
</application>

@ -1,19 +1,100 @@
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 com.gyf.csams.uikit.BackgroundImage
import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.DiskLogAdapter
import com.orhanobut.logger.Logger
class APP : Application() {
private lateinit var memoryCache: LruCache<BackgroundImage, Bitmap>
// 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()
}else{
Logger.i("从缓存读取:${image}")
bitmap.asImageBitmap()
}
}
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)
}
}
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
}
}
return inSampleSize
}
override fun onCreate() {
super.onCreate()
memoryCache = object : LruCache<BackgroundImage, Bitmap>(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.APP_NAME}启动")
}
}

@ -10,8 +10,8 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import com.gyf.csams.account.ui.AccountActivity
import com.gyf.csams.ui.AnimationText
import com.gyf.csams.ui.MainActivity
import com.gyf.csams.main.ui.MainActivity
import com.gyf.csams.uikit.AnimationText
import com.orhanobut.logger.Logger
class InitActivity : ComponentActivity() {
@ -50,7 +50,7 @@ private fun Init(initViewModel:InitViewModel= viewModel()){
when (isValid) {
false -> context.startActivity(Intent(context, AccountActivity::class.java))
true -> context.startActivity(Intent(context,MainActivity::class.java))
true -> context.startActivity(Intent(context, MainActivity::class.java))
}
}

@ -31,8 +31,8 @@ import androidx.navigation.compose.rememberNavController
import com.gyf.csams.BuildConfig
import com.gyf.csams.account.model.AccountViewModel
import com.gyf.csams.account.model.DialogMessage
import com.gyf.csams.ui.AnimationText
import com.gyf.csams.ui.theme.CSAMSTheme
import com.gyf.csams.uikit.AnimationText
import com.gyf.csams.uikit.theme.CSAMSTheme
enum class AccountRoute{
@ -178,7 +178,7 @@ private fun Account(accountViewModel: AccountViewModel = viewModel(),
/**
* 学号
*
*TODO 需要把逻辑封装到[com.gyf.csams.uikit.BaseTextField]
* @param accountViewModel
*/
@Composable
@ -297,9 +297,10 @@ private fun PasswordDialog(accountViewModel: AccountViewModel = viewModel(), mes
}
/**
* 姓名文本框
*
* TODO 需要把逻辑封装到[com.gyf.csams.uikit.BaseTextField]
* @param name 姓名
* @param accountViewModel
*/
@ -328,7 +329,7 @@ private fun Name(name:String, accountViewModel: AccountViewModel = viewModel()){
/**
* 密码框
*
*TODO 需要把逻辑封装到[com.gyf.csams.uikit.BaseTextField]
* @param accountViewModel
*/
@Composable

@ -0,0 +1,45 @@
package com.gyf.csams.association.model
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.gyf.csams.uikit.StringForm
data class Image(val uri:Uri,val createTime:Long,val size:Long)
class RegAssociationViewModel : ViewModel() {
val frameDesc="社团注册资料"
val name= StringForm(formDesc = "社团名称",textLength = 5)
val desc = StringForm(formDesc = "社团简介",textLength = 30)
val _picture=MutableLiveData<Uri>()
val picture:LiveData<Uri> =_picture
val piciurePlaceHolder="请上传图片"
val errorPicture="图片加载失败,请联系管理员"
val deninedPermission="拒绝授权"
val register="注册"
val back="返回"
fun setPicture(uri: Uri){
_picture.value=uri
}
/**
* TODO 注册社团
*
* @param callback
*/
fun register(callback: (value: String) -> Unit){
callback("功能尚未实现,敬请期待")
}
}

@ -0,0 +1,258 @@
package com.gyf.csams.association.ui
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.gyf.csams.R
import com.gyf.csams.association.model.RegAssociationViewModel
import com.gyf.csams.uikit.*
import com.gyf.csams.uikit.theme.CSAMSTheme
import com.orhanobut.logger.Logger
/**
* 注册社团
*
*/
class RegAssociationActivity: ComponentActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CSAMSTheme {
Body()
}
}
}
}
@Composable
fun Body(model:RegAssociationViewModel= viewModel()){
val scaffoldState = rememberScaffoldState()
Scaffold(scaffoldState = scaffoldState) {
Surface(color = MaterialTheme.colors.background) {
MainFrame(background = { Background(BackgroundImage.reg_association,alpha = 0.7F) }) {
Spacer(
modifier = Modifier
.weight(0.1F)
)
Title(model = model)
Name(model = model)
Desc(
model = model, modifier = Modifier
.weight(0.1F)
.fillMaxWidth()
)
Spacer(modifier = Modifier.weight(0.05F))
Logo(
model = model, modifier = Modifier
.weight(0.2F)
.fillMaxWidth()
)
Spacer(modifier = Modifier.weight(0.05F))
BottomButton(modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.weight(0.05F))
ShowSnackbar(scaffoldState = scaffoldState)
}
}
}
}
//@Composable
fun PermissionHandle(context: Context, launcher: ActivityResultLauncher<String>,onGranted:()->Unit){
// Check permission
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_EXTERNAL_STORAGE
) -> {
// Some works that require permission
onGranted()
}
else -> {
// Asking for permission
launcher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
}
/**
* 社团Logo
*
* @param modifier
*/
@Composable
fun Logo(model:RegAssociationViewModel= viewModel(),modifier: Modifier) {
val photoIntent=Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
photoIntent.type = "image/*"
val uri:Uri? by model.picture.observeAsState()
val resultLauncher=rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
when(it.resultCode){
Activity.RESULT_OK->{
Logger.i("uri=${it.data?.data}")
it.data?.data?.let { it1 -> model.setPicture(it1) }
}
}
}
val loadPicture={
//model.loadPicture(context)
resultLauncher.launch(photoIntent)
}
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// Permission Accepted: Do something
loadPicture()
} else {
// Permission Denied: Do something
Logger.w(model.deninedPermission)
}
}
val context= LocalContext.current
Box(contentAlignment = Alignment.Center,modifier = modifier) {
Row(verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,modifier = Modifier
.fillMaxSize()
.border(width = 1.dp, color = Color.Black)) {
if (uri == null) {
OutlinedButton(onClick = {
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_EXTERNAL_STORAGE
) -> {
// Some works that require permission
loadPicture()
}
else -> {
// Asking for permission
launcher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
}) {
Text(text = model.piciurePlaceHolder)
}
} else {
uri.let {
if(it!=null){
Row {
Image(bitmap = BitmapFactory.decodeStream(context.contentResolver.openInputStream(it))
.asImageBitmap(), contentDescription = null)
IconButton(onClick = {
loadPicture()
}) {
Image(painter = painterResource(id = R.drawable.ic_exchange_rate), contentDescription = null)
}
}
}else{
Text(text = model.errorPicture)
}
}
}
}
}
}
@Composable
fun BottomButton(modifier: Modifier=Modifier,scaffoldModel: ScaffoldModel= viewModel(),model:RegAssociationViewModel= viewModel()){
val context= LocalContext.current as RegAssociationActivity
Row(modifier = modifier,horizontalArrangement = Arrangement.Center) {
OutlinedButton(onClick = {
model.register { scaffoldModel.update(it) }
},modifier = Modifier.background(color = MaterialTheme.colors.primary)) {
Text(text = model.register)
}
Spacer(modifier = Modifier.width(10.dp))
OutlinedButton(onClick = {
context.onBackPressed()
},modifier = Modifier.background(color = MaterialTheme.colors.secondary)) {
Text(text = model.back)
}
}
}
/**
* 菜单标题
*
*/
@Composable
fun Title(model:RegAssociationViewModel= viewModel()){
Row(modifier = Modifier.fillMaxWidth(),horizontalArrangement = Arrangement.Center) {
Text(text = model.frameDesc,style = MaterialTheme.typography.h4)
}
}
/**
* 社团名称
* @param model
*/
@Composable
fun Name(model:RegAssociationViewModel= viewModel()){
BaseTextField(form = model.name,singeLine = true,modifier = Modifier.fillMaxWidth())
}
/**
* 社团简介
* @param model
*/
@Composable
fun Desc(model:RegAssociationViewModel= viewModel(),modifier:Modifier){
BaseTextField(form = model.desc,modifier = modifier)
}
@Preview
@Composable
fun NamePreview(){
val model=RegAssociationViewModel()
Body(model=model)
}

@ -1,10 +1,11 @@
package com.gyf.csams.ui.model
package com.gyf.csams.main.model
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.gyf.csams.R
import com.gyf.csams.uikit.StringForm
import com.orhanobut.logger.Logger
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -20,7 +21,7 @@ class MarqueeViewModel:ViewModel() {
private val _marqueeIndex=MutableLiveData(0)
var marqueeIndex:LiveData<Int> = _marqueeIndex
var marqueeJob:Job? = null
private var marqueeJob:Job? = null
@ -38,6 +39,10 @@ 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)
@ -45,13 +50,13 @@ class CarouselViewModel:ViewModel(){
val index:LiveData<Int> = _index
var job:Job? = null
private var job:Job? = null
init {
start()
}
fun start(){
private fun start(){
job = viewModelScope.launch {
do{
_index.postValue(if (_index.value==imageList.size-1) 0 else _index.value?.plus(1))
@ -60,50 +65,68 @@ class CarouselViewModel:ViewModel(){
}
}
fun stop(){
println("停止更新")
job?.cancel()
}
}
/**
* 社团
*
* @property name 社团名称
*/
data class AssociationDto(val name:String)
/**
* 主页
*
*/
class MainViewModel:ViewModel(){
/**
* TODO 发送留言
*
*/
fun sendMessage(callback: (value: String) -> Unit){
callback("功能尚未实现,敬请期待")
}
/**
* TODO 打开通知
*
* @param callback
*/
fun openNotification(callback: (value: String) -> Unit){
callback("功能尚未实现,敬请期待")
}
}
/**
* 社团列表
*
*/
class ListViewModel:ViewModel(){
private val _name=MutableLiveData("")
val name:LiveData<String> = _name
val nameDesc="社团名称"
val namePlaceholder="请输入$nameDesc"
val name = StringForm(formDesc = "社团名称",textLength = 5)
private val _desc=MutableLiveData("")
val desc:LiveData<String> = _desc
val descDesc="社团简介"
val descPlaceholder="请输入$descDesc"
val desc = StringForm(formDesc = "社团简介",textLength = 10)
//注册请求响应信息
private val _snackBarMsg=MutableLiveData<String>()
val snackBarMsg:LiveData<String> = _snackBarMsg
//社团列表加载数量
val associationListSize=10
//社团列表
private val _associationList=MutableLiveData<MutableList<AssociationDto>>(mutableListOf())
val associationDto:LiveData<MutableList<AssociationDto>> = _associationList
val searchDesc="搜索"
init {
loadAssociation()
}
fun onChangeName(name:String){
_name.value=name
}
fun onChangeDesc(desc:String){
_desc.value=desc
}
fun search(){
Logger.i("使用社团名称:${_name.value},社团简介:${_desc.value} 搜索社团")
_snackBarMsg.value="搜索失败,请联系管理员"
/**
* TODO 社团检索
*
* @param callback
*/
fun search(callback: (value: String) -> Unit){
Logger.i("搜索条件[社团名称:${name.formValue.value},社团简介:${desc.formValue.value}]")
callback("功能尚未实现,敬请期待")
}
/**
@ -131,7 +154,7 @@ class ListViewModel:ViewModel(){
* 加载更多社团列表
*
*/
fun addMore(){
fun addMore(callback:(message:String) -> Unit){
viewModelScope.launch {
val c = _associationList.value
@ -147,15 +170,26 @@ class ListViewModel:ViewModel(){
Logger.i("t.size=${t.size}")
_associationList.postValue(t)
Logger.i("加载更多社团size=${_associationList.value?.size}")
_snackBarMsg.value="成功加载更多社团"
callback("成功加载更多社团")
}
}
}
}
fun reset(){
_snackBarMsg.value=""
/**
* 个人中心
*
*/
class CenterViewModel:ViewModel(){
val myAssociationDesc="我的社团"
/**
* TODO 打开我的社团
*
* @param callback
*/
fun openMyAssociation(callback: (value: String) -> Unit){
callback("功能尚未实现,敬请期待")
}
}

@ -1,28 +1,25 @@
package com.gyf.csams.ui
package com.gyf.csams.main.ui
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -32,13 +29,10 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.gyf.csams.R
import com.gyf.csams.ui.model.AssociationDto
import com.gyf.csams.ui.model.CarouselViewModel
import com.gyf.csams.ui.model.ListViewModel
import com.gyf.csams.ui.model.MarqueeViewModel
import com.gyf.csams.ui.theme.CSAMSTheme
import com.orhanobut.logger.Logger
import kotlinx.coroutines.launch
import com.gyf.csams.association.ui.RegAssociationActivity
import com.gyf.csams.main.model.*
import com.gyf.csams.uikit.*
import com.gyf.csams.uikit.theme.CSAMSTheme
/**
@ -48,11 +42,14 @@ import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CSAMSTheme {
Body()
}
}
}
}
@ -67,25 +64,15 @@ fun Body() {
NavHost(navController, startDestination = MainMenu.Main.name) {
composable(MainMenu.Main.name) {
Main(navController = navController)
ShowSnackbar(scaffoldState = scaffoldState)
}
composable(MainMenu.List.name) {
AssociationList(navController = navController)
val model:ListViewModel= viewModel()
val snackBarMsg:String by model.snackBarMsg.observeAsState("")
if(snackBarMsg!=""){
val scope= rememberCoroutineScope()
scope.launch {
scaffoldState.snackbarHostState.showSnackbar(
message = snackBarMsg
)
model.reset()
}
}
ShowSnackbar(scaffoldState = scaffoldState)
}
composable(MainMenu.Center.name) {
Center(navController = navController)
ShowSnackbar(scaffoldState = scaffoldState)
}
}
}
@ -98,16 +85,20 @@ fun Body() {
*
*/
@Composable
fun Center(navController: NavController){
MainFrame(background = { CenterBackground() }, mainMenu = MainMenu.Center, nav = navController) {
fun Center(model:CenterViewModel= viewModel(), scaffoldModel: ScaffoldModel= viewModel(), navController: NavController){
MainFrame(background = { Background(image = BackgroundImage.center,alpha = 0.5F) }, mainMenu = MainMenu.Center, nav = navController) {
Column(modifier = Modifier
.weight(0.33F)
.fillMaxWidth(),verticalArrangement = Arrangement.Bottom) {
Card(backgroundColor = Color.White) {
Row(modifier = Modifier.fillMaxWidth(),verticalAlignment = Alignment.CenterVertically) {
Card(backgroundColor = MaterialTheme.colors.background) {
Row(modifier = Modifier
.fillMaxWidth()
.clickable(onClick = {
model.openMyAssociation { scaffoldModel.update(it) }
}),verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.weight(0.33F))
Row(modifier = Modifier.weight(0.33F),horizontalArrangement = Arrangement.Center) {
Text(text = "我的社团")
Text(text = model.myAssociationDesc)
}
Row(modifier = Modifier.weight(0.33F),horizontalArrangement = Arrangement.End) {
Icon(
@ -126,27 +117,12 @@ fun Center(navController: NavController){
}
}
/**
* 个人中心背景
*
*/
@Composable
fun CenterBackground(){
Image(
painter = painterResource(id = R.drawable.mb_bg_fb_28),
contentDescription = null,
contentScale = ContentScale.FillHeight,
alpha = 0.5F,
modifier = Modifier.fillMaxSize()
)
}
/**
* 主界面
*/
@Composable
fun Main(navController: NavController) {
MainFrame(background = { MainBackground() }, mainMenu = MainMenu.Main, nav = navController) {
MainFrame(background = { Background(image = BackgroundImage.main) }, mainMenu = MainMenu.Main, nav = navController) {
Column(modifier = Modifier.weight(0.33F)) {
Notification()
MessageBoard()
@ -160,19 +136,6 @@ fun Main(navController: NavController) {
}
}
/**
* 主界面背景
*
*/
@Composable
fun MainBackground() {
Image(
painter = painterResource(id = R.drawable.mb_bg_fb_08),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier.fillMaxSize()
)
}
/**
* 社团列表
@ -182,9 +145,7 @@ fun MainBackground() {
@Composable
fun AssociationList(navController: NavController) {
MainFrame(
background = {
AssociationListBackground()
},
background = { Background(image = BackgroundImage.list) },
mainMenu = MainMenu.List,
nav = navController
) {
@ -195,37 +156,28 @@ fun AssociationList(navController: NavController) {
}
/**
* 添加社团按钮
* 注册社团按钮
*
*/
@Composable
fun RegisterAssociation() {
val context= LocalContext.current
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
IconButton(onClick = {
context.startActivity(Intent(context, RegAssociationActivity::class.java))
}) {
Icon(
painter = painterResource(id = R.drawable.ic_add_fill),
contentDescription = null,
modifier = Modifier.size(50.dp)
modifier = Modifier.size(50.dp),
)
}
}
/**
* 社团列表背景
*
*/
@Composable
fun AssociationListBackground() {
Image(
painter = painterResource(id = R.drawable.mb_bg_fb_07),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier.fillMaxSize()
)
}
}
/**
@ -233,7 +185,7 @@ fun AssociationListBackground() {
*
*/
@Composable
fun AssociationListBody(model: ListViewModel = viewModel()) {
fun AssociationListBody(model: ListViewModel = viewModel(),scaffoldModel: ScaffoldModel= viewModel()) {
val associationList: MutableList<AssociationDto>? by model.associationDto.observeAsState()
val listState = rememberLazyListState()
@ -253,7 +205,7 @@ fun AssociationListBody(model: ListViewModel = viewModel()) {
}else{
Box(modifier = Modifier
.weight(0.35F)
.border(width = 1.dp, color = Color.Black))
.border(width = 1.dp, color = MaterialTheme.colors.onBackground))
}
Spacer(modifier = Modifier.weight(0.1F))
}
@ -263,15 +215,9 @@ fun AssociationListBody(model: ListViewModel = viewModel()) {
}
}
}
Logger.i("totalItemsCount=${listState.layoutInfo.totalItemsCount}" +
",firstVisibleItemIndex=${listState.firstVisibleItemIndex}," +
"firstVisibleItemScrollOffset=${listState.firstVisibleItemScrollOffset}," +
"viewportStartOffset=${listState.layoutInfo.viewportStartOffset}" +
"viewportEndOffset=${listState.layoutInfo.viewportEndOffset}")
if(listState.layoutInfo.totalItemsCount-listState.firstVisibleItemIndex==4){
model.addMore()
if(listState.layoutInfo.totalItemsCount-listState.firstVisibleItemIndex==model.associationListSize/2-1){
model.addMore { scaffoldModel.update(it) }
}
}
@ -296,48 +242,23 @@ fun Association(associationDto: AssociationDto) {
*
*/
@Composable
fun AssociationSearch(model: ListViewModel = viewModel()) {
val name: String by model.name.observeAsState("")
fun AssociationSearch(model: ListViewModel = viewModel(),scaffoldModel: ScaffoldModel= viewModel()) {
Card(modifier = Modifier.padding(horizontal = 50.dp, vertical = 10.dp)) {
Column {
Row {
val focusManager = LocalFocusManager.current
Spacer(modifier = Modifier.weight(0.05F))
OutlinedTextField(
modifier = Modifier.weight(0.4F), value = name,
onValueChange = { model.onChangeName(it) },
singleLine = true,
label = { Text(text = model.nameDesc) },
placeholder = { Text(text = model.namePlaceholder) },
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done)
)
Spacer(modifier = Modifier.weight(0.1F))
OutlinedTextField(
modifier = Modifier.weight(0.4F), value = name,
onValueChange = { model.onChangeDesc(it) },
singleLine = true,
label = { Text(text = model.descDesc) },
placeholder = { Text(text = model.descPlaceholder) },
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done)
)
Spacer(modifier = Modifier.weight(0.05F))
}
BaseTextField(form = model.name,singeLine = true,modifier = Modifier.padding(horizontal = 10.dp))
BaseTextField(form = model.desc,singeLine = true,modifier = Modifier.padding(horizontal = 10.dp))
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(10.dp)
modifier = Modifier.height(10.dp)
)
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
OutlinedButton(onClick = { model.search() }, modifier = Modifier.width(100.dp)) {
Text(text = "搜索")
OutlinedButton(onClick = { model.search { scaffoldModel.update(it) } }, modifier = Modifier.width(100.dp)) {
Text(text = model.searchDesc)
}
}
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(10.dp)
modifier = Modifier.height(10.dp)
)
}
@ -350,19 +271,23 @@ fun AssociationSearch(model: ListViewModel = viewModel()) {
*
*/
@Composable
fun Notification() {
fun Notification(mainViewModel: MainViewModel= viewModel(),scaffoldModel: ScaffoldModel= viewModel()) {
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
IconButton(onClick = {
mainViewModel.openNotification { scaffoldModel.update(it) }
}) {
Icon(
painter = painterResource(id = R.drawable.ic_notification),
contentDescription = null,
modifier = Modifier.size(50.dp)
contentDescription = null
)
}
}
}
/**
@ -376,7 +301,7 @@ fun MyBorder(content: @Composable BoxScope.() -> Unit) {
modifier = Modifier
.border(
width = 1.dp,
color = Color.Black,
color = MaterialTheme.colors.onBackground,
shape = RoundedCornerShape(size = 20.dp)
),
) {
@ -390,18 +315,19 @@ fun MyBorder(content: @Composable BoxScope.() -> Unit) {
*
*/
@Composable
fun MessageBoard(model: MarqueeViewModel = viewModel()) {
fun MessageBoard(model: MarqueeViewModel = viewModel(),mainViewModel:MainViewModel= viewModel(),scaffoldModel: ScaffoldModel= viewModel()) {
MyBorder {
Row(
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = { mainViewModel.sendMessage { scaffoldModel.update(it) } }) {
Icon(
painter = painterResource(id = R.drawable.ic_comments),
contentDescription = null,
modifier = Modifier
.size(50.dp)
)
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier

@ -1,4 +1,4 @@
package com.gyf.csams.ui
package com.gyf.csams.uikit
import androidx.annotation.DrawableRes
import androidx.compose.animation.Crossfade
@ -6,14 +6,21 @@ import androidx.compose.animation.animateColor
import androidx.compose.animation.core.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -21,10 +28,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.compose.navigate
import androidx.navigation.compose.rememberNavController
import com.gyf.csams.APP
import com.gyf.csams.R
import com.gyf.csams.ui.model.CarouselViewModel
import com.gyf.csams.ui.model.MarqueeViewModel
import com.gyf.csams.ui.theme.CSAMSTheme
import com.gyf.csams.main.model.CarouselViewModel
import com.gyf.csams.main.model.MarqueeViewModel
import com.gyf.csams.uikit.theme.CSAMSTheme
import com.orhanobut.logger.Logger
import kotlinx.coroutines.launch
/**
* 淡入淡出并且颜色变化文本
@ -35,8 +45,8 @@ import com.gyf.csams.ui.theme.CSAMSTheme
fun AnimationText(text:String){
val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Green,
initialValue = MaterialTheme.colors.primary,
targetValue = MaterialTheme.colors.onPrimary,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
@ -94,7 +104,7 @@ fun MenuIconButton(_menu: MainMenu,menu: MainMenu,modifier: Modifier,onClick: ()
*/
@Composable
fun MainBottomAppBar(menu:MainMenu, nav: NavController, modifier: Modifier=Modifier){
BottomAppBar(backgroundColor = Color.White,modifier=modifier) {
BottomAppBar(backgroundColor = MaterialTheme.colors.background,modifier=modifier) {
//图标宽度平等分
val weight=1/(MainMenu.values().size*1.0f)
@ -146,7 +156,12 @@ fun Marquee(model: MarqueeViewModel = viewModel(), content: @Composable BoxScope
}
}
/**
* 跑马灯布局
*
* @param model
* @param offset
*/
@Composable
fun MarqueeText(model: MarqueeViewModel = viewModel(), offset: State<Float>) {
val poetryIndex: Int by model.marqueeIndex.observeAsState(0)
@ -163,7 +178,7 @@ fun MarqueeText(model: MarqueeViewModel = viewModel(), offset: State<Float>) {
}
/**
* 界面框架
* 导航界面框架
*
* @param background 背景
* @param mainMenu 菜单
@ -184,6 +199,20 @@ fun MainFrame( background:@Composable ()->Unit,mainMenu: MainMenu,nav: NavContro
}
}
/**
* 界面框架
*
* @param background
* @param body
*/
@Composable
fun MainFrame(background:@Composable ()->Unit,body:@Composable ColumnScope.()->Unit){
Box(modifier = Modifier.fillMaxSize()) {
background()
Column(content = body)
}
}
/**
* 图片轮播
*
@ -200,34 +229,89 @@ fun Carousel(
}
}
@Preview
/**
* 通用文本输入框
*
* @param T
* @param modifier
* @param form
* @param singeLine
*/
@Composable
fun CarouselPreview() {
val model = CarouselViewModel()
// ClubActivitiesImage(model = model)
Carousel(model = model) {
Card(
modifier = Modifier
.height(300.dp)
.fillMaxWidth()
// .rotate(180F)
) {
Image(
painter = painterResource(id = R.drawable.hot_activity_background),
contentDescription = null
)
fun <T:StringForm> BaseTextField(modifier:Modifier=Modifier,form:T, singeLine:Boolean=false){
val name: String by form.formValue.observeAsState("")
val focusManager = LocalFocusManager.current
OutlinedTextField(
modifier = modifier,
value = name,
onValueChange = {form.onChange(it)},
label={ Text(text = form.formDesc)},
placeholder = { Text(text = form.formPlaceholder)},
singleLine = singeLine,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
trailingIcon={ Text(text = "${name.length}/${form.textLength}") })
}
Image(
painter = painterResource(id = it),
contentDescription = null
/**
* 底部提示
*
* @param model
* @param scaffoldState
*/
@Composable
fun ShowSnackbar(model:ScaffoldModel= viewModel(),scaffoldState: ScaffoldState){
val message:String? by model.message.observeAsState()
message?.let {
Logger.i("message=$it")
LaunchedEffect(scaffoldState){
launch {
scaffoldState.snackbarHostState.showSnackbar(
message = it
)
model.update()
}
}
}
}
/**
* 界面背景
*
* @property id 资源id
*/
enum class BackgroundImage(@DrawableRes val id:Int){
//主页
main(R.drawable.mb_bg_fb_08),
//社团列表
list(R.drawable.mb_bg_fb_07),
//个人中心
center(R.drawable.mb_bg_fb_28),
//注册社团
reg_association(R.drawable.mb_bg_fb_07)
}
/**
* 界面背景图
*
* @param image
* @param alpha
*/
@Composable
fun Background(image: BackgroundImage, alpha:Float= DefaultAlpha){
val app= LocalContext.current.applicationContext as APP
BoxWithConstraints {
Image(
bitmap = app.getImage(image = image,reqHeight = maxHeight.value.toInt()/2,reqWidth = maxWidth.value.toInt()/2),
contentDescription = null,
contentScale = ContentScale.FillHeight,
alpha = alpha,
modifier = Modifier.fillMaxSize()
)
}
}
@Preview
@Composable
@ -235,7 +319,7 @@ fun AnimationTextPreview(){
AnimationText(text = "6666")
}
@Preview
//@Preview
@Composable
fun MyBottomAppBarPreview(){
val nav= rememberNavController()

@ -0,0 +1,41 @@
package com.gyf.csams.uikit
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.orhanobut.logger.Logger
interface FormLength{
val nameLengthError:String
}
abstract class FormName<T>(val formDesc:String){
protected val _formValue= MutableLiveData<T>()
val formValue: LiveData<T> = _formValue
val formPlaceholder="请输入$formDesc"
abstract fun onChange(value:T)
}
open class StringForm(formDesc: String, val textLength: Int) : FormName<String>(formDesc = formDesc),
FormLength {
override val nameLengthError="${formDesc}不能超过最大长度$textLength"
override fun onChange(value: String) {
if(value.length>textLength){
_formValue.value=value.slice(IntRange(0,textLength-1))
}else{
_formValue.value=value
}
Logger.i("${formDesc}更新值:${_formValue.value}")
}
}
class ScaffoldModel:ViewModel(){
private val _message=MutableLiveData<String>()
val message:LiveData<String> = _message
fun update(message:String?=null){
_message.value=message
}
}

@ -1,4 +1,4 @@
package com.gyf.csams.ui.theme
package com.gyf.csams.uikit.theme
import androidx.compose.ui.graphics.Color

@ -1,4 +1,4 @@
package com.gyf.csams.ui.theme
package com.gyf.csams.uikit.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes

@ -1,4 +1,4 @@
package com.gyf.csams.ui.theme
package com.gyf.csams.uikit.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme

@ -1,4 +1,4 @@
package com.gyf.csams.ui.theme
package com.gyf.csams.uikit.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle

File diff suppressed because one or more lines are too long

@ -39,12 +39,5 @@ class ExampleUnitTest {
println( LocalDateTime.now().get(ChronoField.YEAR))
}
@Test
fun testChunked(){
IntRange(0,13).chunked(3){
println(it)
}
}
}

Loading…
Cancel
Save