自定义Gson序列化反序列化题目实体类逻辑

SnackBar增加操作属性和回调配置
增加社团题库管理
master
pan 3 years ago
parent e8dfc71a90
commit 54ecee40c0
  1. 6
      app/build.gradle.kts
  2. 2
      app/src/androidTest/java/com/gyf/csams/ExampleInstrumentedTest.kt
  3. 5
      app/src/main/AndroidManifest.xml
  4. 227
      app/src/main/java/com/gyf/csams/association/model/ExamViewModel.kt
  5. 6
      app/src/main/java/com/gyf/csams/association/ui/AssociationActivity.kt
  6. 306
      app/src/main/java/com/gyf/csams/association/ui/ExamActivity.kt
  7. 2
      app/src/main/java/com/gyf/csams/association/ui/ReNameActivity.kt
  8. 2
      app/src/main/java/com/gyf/csams/association/ui/RegAssociationActivity.kt
  9. 8
      app/src/main/java/com/gyf/csams/main/ui/MainActivity.kt
  10. 229
      app/src/main/java/com/gyf/csams/uikit/BaseView.kt
  11. 23
      app/src/main/java/com/gyf/csams/uikit/ViewModel.kt
  12. 111
      app/src/main/java/com/gyf/csams/util/GsonUtil.kt
  13. 12
      app/src/main/java/com/gyf/csams/util/HttpUtil.kt
  14. 9
      app/src/main/res/drawable/ic_add_select.xml
  15. 9
      app/src/main/res/drawable/ic_sami_select.xml
  16. 11
      app/src/test/java/com/gyf/csams/ExampleUnitTest.kt
  17. 3
      build.gradle.kts

@ -26,7 +26,7 @@ android {
val serverAddress=rootProject.extra["SERVER_ADDRESS"]
debug {
manifestPlaceholders.apply {
this["APP_NAME"] = appName;
this["APP_NAME"] = appName
}
buildConfigField(type="String",name="APP_NAME",value = "\"$appName\"")
buildConfigField(type="String",name="SERVER_ADDRESS",value = "\"$serverAddress\"")
@ -38,7 +38,7 @@ android {
"proguard-rules.pro"
)
manifestPlaceholders.apply {
this["APP_NAME"] = appName;
this["APP_NAME"] = appName
}
buildConfigField(type="String",name="APP_NAME",value = "\"$appName\"")
buildConfigField(type="String",name="SERVER_ADDRESS",value = "\"$serverAddress\"")
@ -122,6 +122,8 @@ dependencies {
* 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
*/

@ -18,5 +18,7 @@ class ExampleInstrumentedTest {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.gyf.csams", appContext.packageName)
}
}

@ -57,6 +57,11 @@
<activity android:name=".association.ui.ReNameActivity"
android:exported="true">
</activity>
<!--题库界面-->
<activity android:name=".association.ui.ExamActivity"
android:exported="true">
</activity>
</application>
</manifest>

@ -0,0 +1,227 @@
package com.gyf.csams.association.model
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.gyf.csams.uikit.ScrollList
import com.gyf.csams.uikit.StringForm
import kotlin.random.Random
/**
* 题型
*
*/
enum class ExamType(val type:String){
//选择题
cq("选择题"),
//开放题
oq("开放题")
}
sealed class Exam{
abstract val examType:ExamType
abstract val question:StringForm
}
/**
* 开放题
*
* @property examType 题型描述
* @property question 问题
*/
data class OpenQuestionsVo(
override val examType: ExamType = ExamType.oq, override val question: StringForm
) : Exam()
/**
* 选择题
*
* @property examType 题型描述
* @property answers 答案
* @property rightAnswer 正确答案
* @property question 问题
*/
data class ChoiceQuestionVo(override val examType: ExamType = ExamType.cq,
val answers:List<StringForm>,
val rightAnswer:Int,
override val question: StringForm
):Exam()
/**
* 题库状态管理
*
*/
/**
* 问题长度
*/
const val QUESTION_TEXT_LENGTH=30
/**
* 选择题选项数
*/
const val ANSWER_SIZE=4
/**
* 答案长度
*
*/
const val ANSWER_TEXT_LENGTH=15
class ExamViewModel:ScrollList<Exam>() {
val questionIsNull: String = "问题不能为空"
val deleteLeastOne: String="至少保留一道题目"
val updateExam="更新题库"
val back="返回"
val menuName="入团题库"
val deleteTip="确定删除此题目?"
val addTip="确定添加此题目?"
val actionLabel="确定"
override val initSize = 10
private val _newExam:MutableLiveData<Exam> = MutableLiveData(createExam(ExamType.cq))
val newExam:LiveData<Exam> = _newExam
init {
load()
}
/**
* 切换题型
*
*/
fun switchType(exam: Exam){
if(exam is ChoiceQuestionVo) _newExam.value=createExam(ExamType.oq) else _newExam.value=createExam(ExamType.cq)
}
/**
* 创建题目
*
* @param type
* @return
*/
private fun createExam(type: ExamType): Exam {
val question=StringForm(formDesc = "问题",textLength = QUESTION_TEXT_LENGTH)
return when(type){
ExamType.cq-> ChoiceQuestionVo(
answers = ('A'..'D').map { StringForm(formDesc = "选项",textLength = ANSWER_TEXT_LENGTH,value = "选项$it") },
rightAnswer = 0,
question = question
)
ExamType.oq-> OpenQuestionsVo(question = question)
}
}
/**
* 更新题目
*
*/
fun update(oldExam: Exam,newExam:Exam){
if(oldExam==_newExam.value){
_newExam.value=newExam
}else{
_data.value?.apply {
this[indexOf(oldExam)]=newExam
val list = mutableListOf<Exam>()
list.addAll(this)
_data.value?.clear()
_data.value=list
}
}
}
/**
* TODO 更新题库
*
* @param callback
*/
fun updateExam(callback: (message: String) -> Unit){
callback("功能尚未实现,敬请期待")
}
/**
* 加载题目
*
*/
override fun load() {
_data.value?.apply {
repeat(initSize) {
if (Random.nextBoolean()){
add(OpenQuestionsVo(question = StringForm(formDesc = "问题",textLength = QUESTION_TEXT_LENGTH,value = "这是一道开放题:$size")))
} else{
add(
ChoiceQuestionVo(
question = StringForm(formDesc = "问题",textLength = QUESTION_TEXT_LENGTH,value = "这是一道选择题:$size"),
answers = ('A'..'D').map { StringForm(formDesc = "选项",textLength = ANSWER_TEXT_LENGTH,value = "选项$it") },
rightAnswer = Random.nextInt(ANSWER_SIZE)
)
)
}
}
}
}
/**
*TODO 加载更多题目
*
* @param callback
*/
override fun loadMore(callback: (message: String) -> Unit) {
// _data.value?.apply {
// val list= mutableListOf<Exam>()
// list.addAll(this)
// list.apply {
// repeat(10) {
// if (Random.nextBoolean()) add(OpenQuestionsVo(question = "这是一道开放题:$size")) else add(
// ChoiceQuestionVo(
// question = "这是一道选择题:$size,请从选项中选出正确答案",
// answers = listOf("选项A", "选项B", "选项C", "选项D"),
// rightAnswer = 3
// )
// )
// }
// }
// _data.postValue(list)
// callback("成功加载更多题目")
// }
// callback("功能尚未实现,敬请期待")
}
fun addQuestion() {
_data.value?.apply {
_newExam.value?.let{
val list= mutableListOf<Exam>()
list.addAll(this)
list.add(it)
_data.postValue(list)
}
_newExam.value=createExam(ExamType.cq)
}
}
/**
* TODO 删除题目
*
*/
fun deleteQuestion(exam: Exam) {
_data.value?.apply {
val list = mutableListOf<Exam>()
remove(exam)
list.addAll(this)
_data.postValue(list)
}
}
}

@ -108,7 +108,7 @@ private fun Search(modifier:Modifier=Modifier, model: MemberViewModel= viewModel
Spacer(modifier = Modifier.weight((spaceWeight)))
BaseTextField(modifier = Modifier.weight(textFieldWeight),form = model.name,singeLine = true)
Spacer(modifier = Modifier.weight(spaceWeight))
OutlinedButton(onClick = { model.search { scaffoldModel.update(it) } },modifier = Modifier.weight(buttonWeight)) {
OutlinedButton(onClick = { model.search { scaffoldModel.update(message=it) } },modifier = Modifier.weight(buttonWeight)) {
Text(text = model.search)
}
Spacer(modifier = Modifier.weight(spaceWeight))
@ -148,7 +148,7 @@ private fun MemberList(modifier: Modifier=Modifier, model: MemberViewModel=viewM
item {
Row(horizontalArrangement = Arrangement.Center,modifier = Modifier.fillMaxWidth()) {
IconButton(onClick = { model.loadMore{
scaffoldModel.update(it)
scaffoldModel.update(message=it)
} }) {
Icon(painter = painterResource(id = R.drawable.ic_arrow_down), contentDescription = null)
}
@ -302,7 +302,7 @@ private fun HistoryActivityList(modifier: Modifier,model:HistoryActViewModel= vi
}
}
if(listState.layoutInfo.totalItemsCount-listState.firstVisibleItemIndex==model.initSize/2-1){
model.loadMore { scaffoldModel.update(it) }
model.loadMore { scaffoldModel.update(message=it) }
}
}

@ -0,0 +1,306 @@
package com.gyf.csams.association.ui
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.gyf.csams.R
import com.gyf.csams.association.model.*
import com.gyf.csams.uikit.*
/**
* 题库管理
*
*/
class ExamActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Body { scaffoldState ->
MainFrame(background = { Background(image = BackgroundImage.exam) }) {
Spacer(modifier = Modifier.weight(0.1F))
Title(modifier = Modifier.weight(0.1F))
Exam(modifier = Modifier.weight(0.8F))
ShowSnackbar(scaffoldState = scaffoldState)
}
}
}
}
}
/**
* 底部按钮
*
*/
@Composable
private fun BottomButton(
modifier: Modifier = Modifier,
model: ExamViewModel = viewModel(),
scaffoldModel: ScaffoldModel = viewModel()
) {
val context = LocalContext.current as ExamActivity
Row(modifier = modifier, horizontalArrangement = Arrangement.Center) {
OutlinedButton(onClick = {
model.updateExam { scaffoldModel.update(message=it) }
}, modifier = Modifier.background(color = MaterialTheme.colors.primary)) {
Text(text = model.updateExam)
}
Spacer(modifier = Modifier.width(10.dp))
OutlinedButton(onClick = {
context.onBackPressed()
}, modifier = Modifier.background(color = MaterialTheme.colors.secondary)) {
Text(text = model.back)
}
}
}
/**
* 标题
*
* @param modifier
* @param model
*/
@Composable
private fun Title(modifier: Modifier = Modifier, model: ExamViewModel = viewModel()) {
Row(horizontalArrangement = Arrangement.Center, modifier = modifier.fillMaxWidth()) {
Text(text = model.menuName, style = MaterialTheme.typography.h4)
}
}
@Composable
private fun ExamChild(it:Exam,examHeight: Dp,isAdd: Boolean=false){
val questionWeight=0.3F
when (it) {
is OpenQuestionsVo -> ExamOQ(
openQuestionsVo = it,
modifier = Modifier.height(examHeight*questionWeight),
isAdd = isAdd
)
is ChoiceQuestionVo -> ExamCQ(
choiceQuestionVo = it,
modifier = Modifier.height(examHeight),
questionWeight = questionWeight,
isAdd = isAdd
)
}
}
/**
* 题目列表
*
* @param modifier
* @param model
*/
@Composable
private fun Exam(
modifier: Modifier = Modifier,
model: ExamViewModel = viewModel(),
scaffoldModel: ScaffoldModel = viewModel(),
examHeight: Dp = 350.dp
) {
val listState = rememberLazyListState()
val data by model.data.observeAsState()
val newExam by model.newExam.observeAsState()
LazyColumn(state = listState, modifier = modifier) {
data?.forEach {
item {
ExamChild(it = it, examHeight = examHeight)
Spacer(modifier = Modifier.height(20.dp))
}
}
newExam?.let {
item {
Column {
OutlinedButton(onClick = { model.switchType(it) }) {
Text(text = "切换到${if (newExam is ChoiceQuestionVo) "开放题" else "选择题"}")
}
ExamChild(it = it, examHeight = examHeight,isAdd = true)
}
}
}
item {
Column {
Divider(color = MaterialTheme.colors.background)
Spacer(modifier = Modifier.height(30.dp))
BottomButton(modifier = Modifier.fillMaxWidth())
}
}
}
if (listState.layoutInfo.totalItemsCount - listState.firstVisibleItemIndex == model.initSize / 2 - 1) {
model.loadMore { scaffoldModel.update(message=it) }
}
}
/**
* 问题
*
* @param modifier
* @param exam
*/
@Composable
private fun Question(modifier: Modifier = Modifier, exam: Exam) {
BaseTextField(
form = exam.question,
modifier = modifier
.fillMaxSize()
.background(color = MaterialTheme.colors.background)
)
}
/**
* 操作按钮
*
*/
@Composable
private fun ActionButton(modifier: Modifier = Modifier, isAdd: Boolean,
model: ExamViewModel= viewModel(),
scaffoldModel: ScaffoldModel= viewModel(),
exam: Exam) {
val list by model.data.observeAsState()
val newExam by model.newExam.observeAsState()
Box(
contentAlignment = Alignment.Center,
modifier = modifier
) {
IconButton(onClick = {
if(isAdd){
if((newExam?.question?.formValue?.value ?: "").isNotEmpty()){
scaffoldModel.update(message=model.addTip,actionLabel = model.actionLabel){
model.addQuestion()
}
}else{
scaffoldModel.update(message = model.questionIsNull)
}
}else{
if(list?.size==1){
scaffoldModel.update(model.deleteLeastOne)
}else{
scaffoldModel.update(message=model.deleteTip,actionLabel = model.actionLabel){
model.deleteQuestion(exam = exam)
}
}
}
}) {
Icon(
painter = painterResource(id = if (isAdd) R.drawable.ic_add_select else R.drawable.ic_sami_select),
contentDescription = null
)
}
}
}
/**
* 开放题
*
* @param openQuestionsVo
*/
@Composable
private fun ExamOQ(
modifier: Modifier = Modifier,
openQuestionsVo: OpenQuestionsVo,
isAdd: Boolean = false
) {
Row(modifier = modifier) {
Question(
exam = openQuestionsVo, modifier = Modifier
.weight(0.8F)
.fillMaxHeight()
)
ActionButton(
modifier = Modifier
.weight(0.2F)
.fillMaxHeight(),
isAdd = isAdd,
exam = openQuestionsVo
)
}
}
/**
* 选择题
*
* @param choiceQuestionVo
*/
@Composable
private fun ExamCQ(
modifier: Modifier = Modifier,
choiceQuestionVo: ChoiceQuestionVo,
isAdd: Boolean = false,
model: ExamViewModel = viewModel(),
questionWeight:Float
) {
Row(modifier = modifier) {
Column(
modifier = Modifier
.weight(0.8F)
.fillMaxHeight()
) {
Question(
exam = choiceQuestionVo,
modifier = Modifier
.fillMaxWidth()
.weight(questionWeight)
)
Card(
backgroundColor = MaterialTheme.colors.secondaryVariant,
modifier = Modifier
.fillMaxWidth()
.weight(1 - questionWeight)
) {
choiceQuestionVo.answers.apply {
Column {
forEach {
Row(modifier = Modifier
.fillMaxWidth()
.weight(1F / ANSWER_SIZE)) {
val answerIndex: Int = indexOf(it)
val click = {
model.update(
oldExam = choiceQuestionVo,
newExam = choiceQuestionVo.copy(rightAnswer = answerIndex)
)
}
val isRightAnswer =
choiceQuestionVo.rightAnswer == answerIndex
RadioButton(selected = isRightAnswer, onClick = click)
BaseTextField(form = it)
}
}
}
}
}
}
ActionButton(
modifier = Modifier
.weight(0.2F)
.fillMaxHeight(),
isAdd = isAdd,
exam = choiceQuestionVo
)
}
}

@ -101,7 +101,7 @@ private fun BottomButton(modifier: Modifier=Modifier,model:RenameViewModel= view
val context= LocalContext.current as ReNameActivity
Spacer(modifier = Modifier.weight(weight))
Row(modifier=Modifier.weight(0.5F)) {
OutlinedButton(onClick = { model.post{scaffoldModel.update(it)} }) {
OutlinedButton(onClick = { model.post{scaffoldModel.update(message=it)} }) {
Text(text = model.postDesc)
}
Spacer(modifier = Modifier.width(10.dp))

@ -179,7 +179,7 @@ private fun BottomButton(modifier: Modifier=Modifier,scaffoldModel: ScaffoldMode
val context= LocalContext.current as RegAssociationActivity
Row(modifier = modifier,horizontalArrangement = Arrangement.Center) {
OutlinedButton(onClick = {
model.register { scaffoldModel.update(it) }
model.register { scaffoldModel.update(message=it) }
},modifier = Modifier.background(color = MaterialTheme.colors.primary)) {
Text(text = model.register)
}

@ -206,7 +206,7 @@ private fun AssociationListBody(model: ListViewModel = viewModel(),scaffoldModel
}
if(listState.layoutInfo.totalItemsCount-listState.firstVisibleItemIndex==model.associationListSize/2-1){
model.loadMore { scaffoldModel.update(it) }
model.loadMore { scaffoldModel.update(message=it) }
}
}
@ -243,7 +243,7 @@ private fun AssociationSearch(model: ListViewModel = viewModel(),scaffoldModel:
modifier = Modifier.height(10.dp)
)
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
OutlinedButton(onClick = { model.search { scaffoldModel.update(it) } }, modifier = Modifier.width(100.dp)) {
OutlinedButton(onClick = { model.search { scaffoldModel.update(message=it) } }, modifier = Modifier.width(100.dp)) {
Text(text = model.searchDesc)
}
}
@ -269,7 +269,7 @@ private fun Notification(mainViewModel: MainViewModel= viewModel(),scaffoldModel
.padding(10.dp)
) {
IconButton(onClick = {
mainViewModel.openNotification { scaffoldModel.update(it) }
mainViewModel.openNotification { scaffoldModel.update(message=it) }
}) {
Icon(
painter = painterResource(id = R.drawable.ic_notification),
@ -312,7 +312,7 @@ private fun MessageBoard(model: MarqueeViewModel = viewModel(),mainViewModel:Mai
) {
IconButton(onClick = { mainViewModel.sendMessage { scaffoldModel.update(it) } }) {
IconButton(onClick = { mainViewModel.sendMessage { scaffoldModel.update(message=it) } }) {
Icon(
painter = painterResource(id = R.drawable.ic_comments),
contentDescription = null,

@ -45,7 +45,7 @@ import kotlinx.coroutines.launch
* @param text
*/
@Composable
fun AnimationText(text:String){
fun AnimationText(text: String) {
val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
initialValue = MaterialTheme.colors.primary,
@ -67,14 +67,18 @@ fun AnimationText(text:String){
* 主菜单
*
*/
enum class MainMenu(@DrawableRes val selectedIcon:Int,
@DrawableRes val unSelectedIcon:Int){
enum class MainMenu(
@DrawableRes val selectedIcon: Int,
@DrawableRes val unSelectedIcon: Int
) {
//主页
Main(R.drawable.ic_home_fill,R.drawable.ic_home),
Main(R.drawable.ic_home_fill, R.drawable.ic_home),
//社团列表
List(R.drawable.ic_all_fill,R.drawable.ic_all),
List(R.drawable.ic_all_fill, R.drawable.ic_all),
//个人中心
Center(R.drawable.ic_account_fill,R.drawable.ic_account)
Center(R.drawable.ic_account_fill, R.drawable.ic_account)
}
/**
@ -86,9 +90,10 @@ enum class MainMenu(@DrawableRes val selectedIcon:Int,
* @param onClick
*/
@Composable
fun MenuIconButton(_menu: MainMenu,menu: MainMenu,modifier: Modifier,onClick: () -> Unit){
Row(modifier = modifier
,horizontalArrangement = Arrangement.Center) {
fun MenuIconButton(_menu: MainMenu, menu: MainMenu, modifier: Modifier, onClick: () -> Unit) {
Row(
modifier = modifier, horizontalArrangement = Arrangement.Center
) {
IconButton(onClick = onClick) {
Icon(
painter = painterResource(id = if (_menu == menu) menu.selectedIcon else menu.unSelectedIcon),
@ -106,19 +111,19 @@ fun MenuIconButton(_menu: MainMenu,menu: MainMenu,modifier: Modifier,onClick: ()
* @param modifier
*/
@Composable
fun MainBottomAppBar(menu:MainMenu, nav: NavHostController, modifier: Modifier=Modifier){
BottomAppBar(backgroundColor = MaterialTheme.colors.background,modifier=modifier) {
fun MainBottomAppBar(menu: MainMenu, nav: NavHostController, modifier: Modifier = Modifier) {
BottomAppBar(backgroundColor = MaterialTheme.colors.background, modifier = modifier) {
//图标宽度平等分
val weight=1/(MainMenu.values().size*1.0f)
val weight = 1 / (MainMenu.values().size * 1.0f)
Row(Modifier.fillMaxWidth()) {
MenuIconButton(_menu = menu,menu = MainMenu.Main,Modifier.weight(weight),
onClick = { nav.navigate(MainMenu.Main.name)})
MenuIconButton(_menu = menu,menu = MainMenu.List,Modifier.weight(weight),
onClick = { nav.navigate(MainMenu.List.name)})
MenuIconButton(_menu = menu,menu = MainMenu.Center,Modifier.weight(weight),
onClick = { nav.navigate(MainMenu.Center.name)})
MenuIconButton(_menu = menu, menu = MainMenu.Main, Modifier.weight(weight),
onClick = { nav.navigate(MainMenu.Main.name) })
MenuIconButton(_menu = menu, menu = MainMenu.List, Modifier.weight(weight),
onClick = { nav.navigate(MainMenu.List.name) })
MenuIconButton(_menu = menu, menu = MainMenu.Center, Modifier.weight(weight),
onClick = { nav.navigate(MainMenu.Center.name) })
}
}
@ -128,7 +133,7 @@ fun MainBottomAppBar(menu:MainMenu, nav: NavHostController, modifier: Modifier=M
* 社团菜单
*
*/
enum class AssociationMenu(val menuName:String){
enum class AssociationMenu(val menuName: String) {
member("社团成员"),
main("社团主页"),
list("活动列表")
@ -139,34 +144,52 @@ enum class AssociationMenu(val menuName:String){
*
*/
@Composable
fun AssociationAppBar(menu: AssociationMenu,nav: NavHostController,back:()->Unit,dropMenu:()->Unit){
TopAppBar(backgroundColor = MaterialTheme.colors.secondary) {
Row(modifier = Modifier.fillMaxWidth(),verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = back,modifier = Modifier.weight(0.1F)) {
Icon(painter = painterResource(id = R.drawable.ic_arrow_left), contentDescription = null)
}
Row(modifier = Modifier
.weight(0.8F)
.fillMaxHeight(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
val menus=AssociationMenu.values()
menus.forEach {
Row(modifier = Modifier
.weight(1F / menus.size)
.clickable(onClick = { nav.navigate(it.name) }),
horizontalArrangement = Arrangement.Center) {
Text(text = it.menuName,color = if(menu==it) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground)
}
}
}
IconButton(onClick = dropMenu,modifier = Modifier.weight(0.1F)) {
Icon(painter = painterResource(id = R.drawable.ic_configuration), contentDescription = null)
}
}
}
fun AssociationAppBar(
menu: AssociationMenu,
nav: NavHostController,
back: () -> Unit,
dropMenu: () -> Unit
) {
TopAppBar(backgroundColor = MaterialTheme.colors.secondary) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = back, modifier = Modifier.weight(0.1F)) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow_left),
contentDescription = null
)
}
Row(
modifier = Modifier
.weight(0.8F)
.fillMaxHeight(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
val menus = AssociationMenu.values()
menus.forEach {
Row(
modifier = Modifier
.weight(1F / menus.size)
.clickable(onClick = { nav.navigate(it.name) }),
horizontalArrangement = Arrangement.Center
) {
Text(
text = it.menuName,
color = if (menu == it) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground
)
}
}
}
IconButton(onClick = dropMenu, modifier = Modifier.weight(0.1F)) {
Icon(
painter = painterResource(id = R.drawable.ic_configuration),
contentDescription = null
)
}
}
}
}
/**
* 跑马灯
*
@ -174,11 +197,14 @@ fun AssociationAppBar(menu: AssociationMenu,nav: NavHostController,back:()->Unit
* @param content
*/
@Composable
fun Marquee(model: MarqueeViewModel = viewModel(), content: @Composable BoxScope.(model: MarqueeViewModel,
value: State<Float>
) -> Unit) {
fun Marquee(
model: MarqueeViewModel = viewModel(), content: @Composable BoxScope.(
model: MarqueeViewModel,
value: State<Float>
) -> Unit
) {
val delayMillis=2000
val delayMillis = 2000
Column {
BoxWithConstraints {
val transition = rememberInfiniteTransition()
@ -232,7 +258,12 @@ fun MarqueeText(model: MarqueeViewModel = viewModel(), offset: State<Float>) {
* @param body 内容
*/
@Composable
fun MainFrame( background:@Composable ()->Unit,mainMenu: MainMenu,nav: NavHostController,body:@Composable ColumnScope.()->Unit){
fun MainFrame(
background: @Composable () -> Unit,
mainMenu: MainMenu,
nav: NavHostController,
body: @Composable ColumnScope.() -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
background()
Column {
@ -252,7 +283,7 @@ fun MainFrame( background:@Composable ()->Unit,mainMenu: MainMenu,nav: NavHostCo
* @param body
*/
@Composable
fun MainFrame(background:@Composable ()->Unit,body:@Composable ColumnScope.()->Unit){
fun MainFrame(background: @Composable () -> Unit, body: @Composable ColumnScope.() -> Unit) {
Box(modifier = Modifier.fillMaxSize()) {
background()
Column(content = body)
@ -284,19 +315,23 @@ fun Carousel(
* @param singeLine
*/
@Composable
fun <T:StringForm> BaseTextField(modifier:Modifier=Modifier,form:T, singeLine:Boolean=false){
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)},
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}") })
trailingIcon = { Text(text = "${name.length}/${form.textLength}") })
}
/**
@ -306,17 +341,34 @@ fun <T:StringForm> BaseTextField(modifier:Modifier=Modifier,form:T, singeLine:Bo
* @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()
fun ShowSnackbar(model: ScaffoldModel = viewModel(), scaffoldState: ScaffoldState) {
val snackBar: SnackBar? by model.data.observeAsState()
snackBar?.apply {
if (message != null) {
LaunchedEffect(scaffoldState) {
launch {
if (actionLabel != null) {
val result=scaffoldState.snackbarHostState.showSnackbar(
message = message, actionLabel = actionLabel,
duration = duration
)
when(result){
SnackbarResult.ActionPerformed->{
Logger.i("点击操作按钮")
callback()
}
SnackbarResult.Dismissed->{
Logger.d("窗口消失")
}
}
} else {
scaffoldState.snackbarHostState.showSnackbar(message = message)
}
model.update()
}
}
}
}
}
@ -327,19 +379,28 @@ fun ShowSnackbar(model:ScaffoldModel= viewModel(),scaffoldState: ScaffoldState){
*
* @property id 资源id
*/
enum class BackgroundImage(@DrawableRes val id:Int){
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_06),
//社团主界面
association_main(R.drawable.mb_bg_fb_25_180),
//社团重命名
rename(R.drawable.mb_bg_fb_27)
rename(R.drawable.mb_bg_fb_27),
//社团题库管理
exam(R.drawable.mb_bg_fb_02)
}
/**
@ -349,12 +410,16 @@ enum class BackgroundImage(@DrawableRes val id:Int){
* @param alpha
*/
@Composable
fun Background(image: BackgroundImage, alpha:Float= DefaultAlpha){
val app= LocalContext.current.applicationContext as APP
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),
bitmap = app.getImage(
image = image,
reqHeight = maxHeight.value.toInt() / 2,
reqWidth = maxWidth.value.toInt() / 2
),
contentDescription = null,
contentScale = ContentScale.FillHeight,
alpha = alpha,
@ -369,12 +434,12 @@ fun Background(image: BackgroundImage, alpha:Float= DefaultAlpha){
* @param content
*/
@Composable
fun Body(content:@Composable (scaffoldState:ScaffoldState)->Unit){
fun Body(content: @Composable (scaffoldState: ScaffoldState) -> Unit) {
Surface(color = MaterialTheme.colors.background) {
val scaffoldState = rememberScaffoldState()
Scaffold(scaffoldState = scaffoldState) {
content(scaffoldState=scaffoldState)
content(scaffoldState = scaffoldState)
}
}
}
@ -385,13 +450,13 @@ fun Body(content:@Composable (scaffoldState:ScaffoldState)->Unit){
* @param content
*/
@Composable
fun Body( content:@Composable (nav:NavHostController, scaffoldState:ScaffoldState)->Unit){
fun Body(content: @Composable (nav: NavHostController, scaffoldState: ScaffoldState) -> Unit) {
Surface(color = MaterialTheme.colors.background) {
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState()
Scaffold(scaffoldState = scaffoldState) {
content(nav=navController,scaffoldState=scaffoldState)
content(nav = navController, scaffoldState = scaffoldState)
}
}
}
@ -401,7 +466,7 @@ fun Body( content:@Composable (nav:NavHostController, scaffoldState:ScaffoldStat
*
*/
@Composable
fun Poster(modifier: Modifier=Modifier,@DrawableRes id: Int){
fun Poster(modifier: Modifier = Modifier, @DrawableRes id: Int) {
Card(
modifier = modifier,
backgroundColor = Color.Transparent
@ -410,12 +475,12 @@ fun Poster(modifier: Modifier=Modifier,@DrawableRes id: Int){
Image(
painter = painterResource(id = R.drawable.hot_activity_background),
contentDescription = null,
modifier=Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize()
)
Image(
painter = painterResource(id = id),
contentDescription = null,
modifier=Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize()
)
}
}
@ -426,7 +491,7 @@ fun Poster(modifier: Modifier=Modifier,@DrawableRes id: Int){
*
*/
@Composable
fun DescCard(modifier: Modifier){
fun DescCard(modifier: Modifier) {
Card(
modifier = modifier,
backgroundColor = Color.Transparent
@ -459,13 +524,13 @@ fun DescCard(modifier: Modifier){
@Preview
@Composable
fun AnimationTextPreview(){
fun AnimationTextPreview() {
AnimationText(text = "6666")
}
//@Preview
@Composable
fun MyBottomAppBarPreview(){
fun MyBottomAppBarPreview() {
CSAMSTheme {
}

@ -1,5 +1,6 @@
package com.gyf.csams.uikit
import androidx.compose.material.SnackbarDuration
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@ -29,6 +30,11 @@ abstract class FormName<T>(val formDesc:String){
*/
open class StringForm(formDesc: String, val textLength: Int) : FormName<String>(formDesc = formDesc),
FormLength {
constructor(formDesc: String,textLength: Int,value:String) : this(formDesc = formDesc,textLength = textLength) {
_formValue.value=value
}
override val nameLengthError="${formDesc}不能超过最大长度$textLength"
override fun onChange(value: String) {
@ -41,16 +47,23 @@ open class StringForm(formDesc: String, val textLength: Int) : FormName<String>(
}
}
data class SnackBar(val message:String?, val actionLabel:String?=null,val duration: SnackbarDuration=SnackbarDuration.Short, val callback: () -> Unit?)
/**
* snackbar
*
*/
class ScaffoldModel:ViewModel(){
private val _message=MutableLiveData<String>()
val message:LiveData<String> = _message
fun update(message:String?=null){
_message.value=message
private val _data=MutableLiveData<SnackBar>()
val data:LiveData<SnackBar> = _data
fun update(message:String?=null,actionLabel: String? = null,callback: () -> Unit? = {}){
if(message==null){
_data.value=null
}else {
_data.value = SnackBar(message = message, actionLabel = actionLabel,callback = callback)
}
}
}

@ -0,0 +1,111 @@
package com.gyf.csams.util
import com.google.gson.*
import com.google.gson.reflect.TypeToken
import com.gyf.csams.association.model.*
import com.gyf.csams.uikit.StringForm
import java.lang.reflect.Type
class OpenQuestionsVoSerializer: JsonSerializer<OpenQuestionsVo> {
override fun serialize(
vo: OpenQuestionsVo?,
p1: Type?,
p2: JsonSerializationContext?
): JsonElement {
val gson= Gson()
val c = JsonObject()
c.add("examType", gson.toJsonTree(vo?.examType?.name))
c.add("question",gson.toJsonTree(vo?.question?.formValue?.value))
return c
}
}
class OpenQuestionsVoDeserializer: JsonDeserializer<OpenQuestionsVo> {
override fun deserialize(
json: JsonElement?,
p1: Type?,
p2: JsonDeserializationContext?
): OpenQuestionsVo {
val root=json?.asJsonObject
val value=root?.get("question")?.asString
val question= StringForm(formDesc = "问题",textLength = QUESTION_TEXT_LENGTH)
if (value != null) {
question.onChange(value)
return OpenQuestionsVo(question = question)
}else{
throw NullPointerException("问题无法解析!!!!")
}
}
}
class ChoiceQuestionVoSerializer: JsonSerializer<ChoiceQuestionVo> {
override fun serialize(
vo: ChoiceQuestionVo?,
p1: Type?,
p2: JsonSerializationContext?
): JsonElement {
val gson= Gson()
val c = JsonObject()
c.add("examType", gson.toJsonTree(vo?.examType?.name))
c.add("question",gson.toJsonTree(vo?.question?.formValue?.value))
c.add("answers",gson.toJsonTree(vo?.answers))
c.add("rightAnswer",gson.toJsonTree(vo?.rightAnswer))
return c
}
}
class ChoiceQuestionVoDeserializer: JsonDeserializer<ChoiceQuestionVo> {
override fun deserialize(
json: JsonElement?,
p1: Type?,
p2: JsonDeserializationContext?
): ChoiceQuestionVo {
val root=json?.asJsonObject
val value=root?.get("question")?.asString
val question=StringForm(formDesc = "问题",textLength = QUESTION_TEXT_LENGTH)
if (value != null) {
question.onChange(value)
val gson=Gson()
val answers:List<String> = gson.fromJson(root.get("answers"),object : TypeToken<List<String>>() {}.type)
if(answers.size!= ANSWER_SIZE){
throw IllegalArgumentException("选项数量!=$QUESTION_TEXT_LENGTH")
}
val rightAnswer=root.get("rightAnswer").asInt
return ChoiceQuestionVo(
question = question,
answers = answers.map { StringForm(formDesc = "选项",textLength = ANSWER_TEXT_LENGTH,value=it) },
rightAnswer = rightAnswer
)
}else{
throw NullPointerException("问题无法解析!!!!")
}
}
}
/**
* 题目反序列化
*
*/
class ExamDeserializer:JsonDeserializer<Exam>{
companion object{
val gson: Gson =GsonBuilder()
.registerTypeAdapter(OpenQuestionsVo::class.java,OpenQuestionsVoDeserializer())
.registerTypeAdapter(ChoiceQuestionVo::class.java,ChoiceQuestionVoDeserializer())
.create()
}
override fun deserialize(json: JsonElement?, p1: Type?, p2: JsonDeserializationContext?): Exam {
val root=json?.asJsonObject
val type=root?.get("examType")?.asString
return when (gson.fromJson(type, ExamType::class.java)) {
ExamType.cq -> gson.fromJson(json,
object : TypeToken<ChoiceQuestionVo>() {}.type)
ExamType.oq -> gson.fromJson(json,
object : TypeToken<OpenQuestionsVo>() {}.type)
null->throw NullPointerException("无法识别题目类型!")
}
}
}

@ -1,6 +1,8 @@
package com.gyf.csams.util
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.gyf.csams.association.model.Exam
import com.orhanobut.logger.Logger
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
@ -93,10 +95,18 @@ object HttpClient{
data class ApiResponse<T>(val code:Int,val message:String,val body:T?=null)
class SimpleCallback<T>(private val action:String,
private val onSuccess:(res:ApiResponse<T>) -> Unit,
private val onFail:(error:String) -> Unit,
private val type: Type):Callback{
companion object{
val gson: Gson =GsonBuilder()
.registerTypeAdapter(Exam::class.java,ExamDeserializer())
.create()
}
override fun onFailure(call: Call, e: IOException) {
onFail("${action}失败,请联系管理员")
Logger.e(e,"${action}请求失败,发生IO异常")
@ -107,7 +117,7 @@ class SimpleCallback<T>(private val action:String,
val body=response.body
if (body!=null&&body.contentType()?.subtype == "json") {
val jsonRes=body.string()
val res:ApiResponse<T> = Gson().fromJson(jsonRes, type)
val res:ApiResponse<T> = gson.fromJson(jsonRes, type)
Logger.i("${action}请求响应成功:")
Logger.json(jsonRes)
onSuccess(res)

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FF000000"
android:pathData="M544,213.33v266.67H810.67v64H544V810.67h-64V544H213.33v-64h266.67V213.33z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FF000000"
android:pathData="M810.67,480v64H213.33v-64z"/>
</vector>

@ -5,8 +5,6 @@ import com.google.gson.reflect.TypeToken
import com.gyf.csams.util.ApiResponse
import org.junit.Assert.assertEquals
import org.junit.Test
import java.time.LocalDateTime
import java.time.temporal.ChronoField
/**
@ -36,7 +34,14 @@ class ExampleUnitTest {
@Test
fun testYear(){
println( LocalDateTime.now().get(ChronoField.YEAR))
repeat(10,{
println(it)
})
}
@Test
fun testCharRange(){
println(('A'..'D').map { "选项$it" })
}
}

@ -8,13 +8,14 @@ buildscript {
val APP_NAME by extra("大学生社团管理系统")
val SERVER_ADDRESS by extra("http://192.168.50.107:8080")
val room_version by extra("2.2.6")
val kotlin_version by extra("1.4.32")
repositories {
maven("https://maven.aliyun.com/repository/google")
maven("https://maven.aliyun.com/repository/public")
}
dependencies {
classpath("com.android.tools.build:gradle:7.0.0-alpha15")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32")
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.kts files

Loading…
Cancel
Save