First Compose layout

This commit is contained in:
Sebastian Seedorf
2021-11-11 18:30:12 +01:00
parent ad2506333b
commit f2133abea6
12 changed files with 254 additions and 174 deletions

View File

@@ -32,10 +32,12 @@ android {
buildFeatures {
viewBinding true
compose true
}
dataBinding {
enabled = true
composeOptions {
kotlinCompilerVersion kotlin_version
kotlinCompilerExtensionVersion compose_version
}
// To inline the bytecode built with JVM target 1.8 into
@@ -54,7 +56,6 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
@@ -77,4 +78,14 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// Compose
implementation "androidx.compose.runtime:runtime:$compose_version"
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
implementation "androidx.compose.foundation:foundation:$compose_version"
implementation "androidx.compose.foundation:foundation-layout:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "com.google.android.material:compose-theme-adapter:$compose_version"
}

View File

@@ -5,7 +5,7 @@ import com.beust.klaxon.KlaxonException
data class Lecturer (
val fistName: String,
val firstName: String,
val lastName: String,
val email: String,
val isResponsible: Boolean

View File

@@ -1,66 +0,0 @@
package de.sebse.fuplanner2.ui.courses
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.database.Course
import de.sebse.fuplanner2.ui.CaptionHolder
import de.sebse.fuplanner2.ui.CustomHolder
import de.sebse.fuplanner2.ui.ListItemHolder
import de.sebse.fuplanner2.ui.ViewHolderGenerator
import de.sebse.fuplanner2.utils.cast
import java.util.*
class CoursesAdapter(private val onclick: (Course) -> Unit) : RecyclerView.Adapter<CustomHolder>() {
private val positionalData: ArrayList<Any> = arrayListOf()
var dataset: List<Course> = listOf()
set(value) {
field = value
positionalData.clear()
var last: Course? = null
dataset.forEach {
if (last?.let { last -> it < last } != false)
positionalData.add(Pair(it.isSummerSemester, it.year))
positionalData.add(it)
last = it
}
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomHolder {
return ViewHolderGenerator.getHolderByType(parent, viewType)
}
override fun onBindViewHolder(holder: CustomHolder, position: Int) {
// val viewType = getItemViewType(position)
val res = holder.itemView.resources
when (holder) {
is CaptionHolder -> cast<Pair<Boolean, Int?>>(positionalData[position])?.let { (isSummer, year) ->
holder.string.text = when {
year == null -> res.getString(R.string.projects)
isSummer -> res.getString(R.string.summer_semester, year)
else -> res.getString(R.string.winter_semester, year, year+1)
}
}
is ListItemHolder -> cast<Course>(positionalData[position])?.let {
holder.title.text = it.title
holder.subLeft.text = it.lecturers.filter { lecturer -> lecturer.isResponsible }.joinToString { lecturer ->
res.getString(R.string.full_name, lecturer.fistName.substring(0, 1)+".", lecturer.lastName)
}
holder.subRight.text = it.type
holder.itemView.setOnClickListener { _ -> this.onclick(it) }
}
}
}
override fun getItemViewType(position: Int): Int {
return when (positionalData[position]) {
is Course -> ViewHolderGenerator.HolderType.ITEM.ordinal
else -> ViewHolderGenerator.HolderType.HEADER.ordinal
}
}
// Return the size of your dataset (invoked by the layout manager)
override fun getItemCount() = positionalData.size
}

View File

@@ -1,25 +1,20 @@
package de.sebse.fuplanner2.ui.courses
import android.os.Bundle
import android.provider.Settings.Global.getString
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.auth.AppAccounts
import de.sebse.fuplanner2.database.Course
import de.sebse.fuplanner2.databinding.ActivityMainBinding
import com.google.android.material.composethemeadapter.MdcTheme
import de.sebse.fuplanner2.databinding.FragmentRefreshRecyclerBinding
import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_ACCOUNT_NAME
import de.sebse.fuplanner2.worker.CourseWorker
class CoursesFragment : Fragment() {
@@ -32,42 +27,23 @@ class CoursesFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val viewManager = LinearLayoutManager(context)
val viewAdapter = CoursesAdapter(this::onClick)
): View {
navController = findNavController()
coursesViewModel = ViewModelProvider(this).get(CoursesViewModel::class.java)
binding = FragmentRefreshRecyclerBinding.inflate(inflater, container, false)
return inflater.inflate(R.layout.fragment_refresh_recycler, container, false).apply {
binding.recyclerView.apply {
//setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
return ComposeView(requireContext()).apply {
setContent {
MdcTheme {
val courses = coursesViewModel.text.observeAsState(listOf())
LazyColumn {
items(courses.value) { course -> CourseItem(course) {
course.uid?.let {
navController.navigate(CoursesFragmentDirections.actionNavHomeToCourseDetails(it, course.title))
}
} }
}
}
}
binding.swipeRefreshLayout.setOnRefreshListener {
val work = OneTimeWorkRequestBuilder<CourseWorker>()
.setInputData(workDataOf(
KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name
))
.build()
WorkManager.getInstance(context.applicationContext)
.enqueue(work)
WorkManager.getInstance(context.applicationContext)
.getWorkInfoByIdLiveData(work.id)
.observe(viewLifecycleOwner, {
if (it.state.isFinished)
binding.swipeRefreshLayout.isRefreshing = false
})
}
coursesViewModel.text.observe(viewLifecycleOwner, Observer {
viewAdapter.dataset = it
})
navController = findNavController()
}
}
private fun onClick(course: Course) {
course.uid?.let {
navController.navigate(CoursesFragmentDirections.actionNavHomeToCourseDetails(it, course.title))
}
}
}

View File

@@ -0,0 +1,154 @@
package de.sebse.fuplanner2.ui.courses
import android.content.Context
import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Person
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.google.android.material.composethemeadapter.MdcTheme
import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.database.Course
import de.sebse.fuplanner2.database.Lecturer
import de.sebse.fuplanner2.utils.color.getColor
@Composable
fun CourseList(courses: List<Course>, onclick: () -> Unit) {
LazyColumn {
items(courses) { course -> CourseItem(course, onclick) }
}
}
@Composable
fun CourseItem(course: Course, onclick: () -> Unit) {
@Suppress("SimplifiableCallChain")
CourseItem(
id = course.uid,
title = course.title,
lecturers = course.lecturers
.filter { lecturer -> lecturer.isResponsible }
.map { lecturer -> stringResource(R.string.full_name, lecturer.firstName.substring(0, 1)+".", lecturer.lastName) }
.joinToString(),
type = course.type,
onclick = onclick
)
}
@Composable
fun CourseItem(id: Long?, title: String, lecturers: String, type: String, onclick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onclick)
.padding(dimensionResource(R.dimen.card_view_margin))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.card_view_padding))
) {
Text(
text = title,
color = Color(getColor(LocalContext.current, id ?: 0, highContrast = true)),
style = MaterialTheme.typography.h6
)
CourseItemHint(
icon = Icons.Outlined.Person,
imageAltRes = R.string.lecturers,
text = lecturers
)
CourseItemHint(
icon = Icons.Outlined.Info,
imageAltRes = R.string.course_type,
text = type
)
}
}
}
@Composable
fun CourseItemHint(icon: ImageVector, @StringRes imageAltRes: Int, text: String) {
Row {
Icon(
icon,
contentDescription = stringResource(imageAltRes),
modifier = Modifier.padding(
start = dimensionResource(R.dimen.card_view_padding)
)
)
Text(
text = text,
style = MaterialTheme.typography.subtitle1,
modifier = Modifier.padding(
start = dimensionResource(R.dimen.card_view_padding)
)
)
}
}
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun CourseListPreview() {
val course = Course(
userId = 1,
lastRefreshed = 1,
lvNumber = hashSetOf("1000"),
title = "Kurs mit tollem Namen",
description = "Beschreibung",
internalId = "165464635",
isSummerSemester = false,
year = 2020,
lecturers = listOf(
Lecturer(
firstName = "Max",
lastName = "Maurer",
email = "some@mail.com",
isResponsible = false
),
Lecturer(
firstName = "Peter",
lastName = "Engelbert",
email = "coolio@example.com",
isResponsible = false
)
),
moduleType = 1236,
type = "Type"
)
MdcTheme {
CourseList(listOf(
course, course, course
)) { }
}
}
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview
@Composable
fun CourseItemPreview() {
MdcTheme {
CourseItem(
12411111111,
"Höhere Algorithmik",
"M. Berta, P. Parker",
"Vorlesung"
) {}
}
}

View File

@@ -61,7 +61,7 @@ class DetailsAdapter(private val onQuickLink: (ButtonTypes) -> Unit, private val
holder.btnResources.setOnClickListener { this.onQuickLink(ButtonTypes.RESOURCES) }
}
is MailHolder -> cast<Lecturer>(positionalData[position])?.let { type ->
holder.title.text = res.getString(R.string.full_name, type.fistName, type.lastName)
holder.title.text = res.getString(R.string.full_name, type.firstName, type.lastName)
holder.subLeft.text = type.email
holder.itemView.setOnClickListener { this.onMailTo(type) }
}

View File

@@ -69,8 +69,8 @@ class DetailsFragment : Fragment() {
intent.type = "text/html"
intent.data = Uri.fromParts("mailto", lecturer.email, null)
intent.putExtra(Intent.EXTRA_SUBJECT, this.title)
intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.email_preview, lecturer.fistName, lecturer.lastName))
startActivity(Intent.createChooser(intent, getString(R.string.send_email, lecturer.fistName, lecturer.lastName)))
intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.email_preview, lecturer.firstName, lecturer.lastName))
startActivity(Intent.createChooser(intent, getString(R.string.send_email, lecturer.firstName, lecturer.lastName)))
}
private fun launchFragment(btnType: DetailsAdapter.ButtonTypes) {

View File

@@ -1,24 +1,21 @@
package de.sebse.fuplanner2.ui.schedule
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.view.*
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.alamkanak.weekview.WeekView
import com.alamkanak.weekview.WeekViewDisplayable
import com.alamkanak.weekview.WeekViewEntity
import com.alamkanak.weekview.WeekViewEvent
import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.database.AppDatabase
import de.sebse.fuplanner2.database.Course
import de.sebse.fuplanner2.database.Event
import de.sebse.fuplanner2.ui.schedule.MyCustomPagingAdapter.LoadMoreHandler
import de.sebse.fuplanner2.utils.color.getColor
import de.sebse.fuplanner2.utils.getHtmlSpannedString
import de.sebse.fuplanner2.utils.toTimeString
import kotlinx.coroutines.Dispatchers
@@ -26,8 +23,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.math.roundToInt
import kotlin.random.Random
class ScheduleFragment : Fragment() {
@@ -164,56 +159,6 @@ data class ContextEvent(val actCtx: Context, val event: Event) {
.setStyle(style)
.build()
}
@ColorInt
fun getColor(actCtx: Context, seed: Long): Int {
var h = Random(seed).nextInt(0xFFFF)
h = h * 360 / 0xffff
//int s = 0xff & encodedHash[2];
//s = s * 100 / 0xffff;
//int v = 0xff & encodedHash[3];
//v = v * 100 / 0xffff;
// range for more beautiful colors
h = h / 30 * 30
val (s, v) =
if (actCtx.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
Pair(100, 20)//Pair(100, 80)
else
Pair(60, 100)
val (r, g, b) = hsvToRgb(h / 360.0, s / 100.0, v / 100.0)
return rgbToColorInt(r, g, b)
}
private fun hsvToRgb(
hue: Double,
saturation: Double,
value: Double
): Triple<Double, Double, Double> {
val h = (hue * 6).toInt()
val f = hue * 6 - h
val p = value * (1 - saturation)
val q = value * (1 - f * saturation)
val t = value * (1 - (1 - f) * saturation)
return when (h) {
0 -> Triple(value, t, p)
1 -> Triple(q, value, p)
2 -> Triple(p, value, t)
3 -> Triple(p, q, value)
4 -> Triple(t, p, value)
5 -> Triple(value, p, q)
else -> Triple(0.0, 0.0, 0.0)
}
}
@ColorInt
private fun rgbToColorInt(
r: Double,
g: Double,
b: Double
): Int {
return 0xFF000000.toInt() + (r*0xFF).roundToInt() * 0x10000 + (g*0xFF).roundToInt() * 0x100 + (b*0xFF).roundToInt()
}
}
class MyCustomPagingAdapter(

View File

@@ -5,11 +5,13 @@ package de.sebse.fuplanner2.utils
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.text.Html
import android.text.Spanned
import android.text.format.DateFormat
import android.util.Log
import androidx.annotation.ColorInt
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.lifecycle.LiveData
@@ -20,6 +22,8 @@ import kotlinx.coroutines.runBlocking
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.roundToInt
import kotlin.random.Random
object console {
@@ -60,6 +64,60 @@ object xml {
}
}
object color {
@ColorInt
fun getColor(actCtx: Context, seed: Long, highContrast: Boolean = false): Int {
var h = Random(seed).nextInt(0xFFFF)
h = h * 360 / 0xffff
//int s = 0xff & encodedHash[2];
//s = s * 100 / 0xffff;
//int v = 0xff & encodedHash[3];
//v = v * 100 / 0xffff;
// range for more beautiful colors
h = h / 30 * 30
val isNightMode = actCtx.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
val doDarkColors = if (highContrast) !isNightMode else isNightMode
val (s, v) =
if (doDarkColors)
Pair(100, 50)//Pair(100, 80)
else
Pair(100, 70)
val (r, g, b) = hsvToRgb(h / 360.0, s / 100.0, v / 100.0)
return rgbToColorInt(r, g, b)
}
@ColorInt
private fun rgbToColorInt(
r: Double,
g: Double,
b: Double
): Int {
return 0xFF000000.toInt() + (r*0xFF).roundToInt() * 0x10000 + (g*0xFF).roundToInt() * 0x100 + (b*0xFF).roundToInt()
}
private fun hsvToRgb(
hue: Double,
saturation: Double,
value: Double
): Triple<Double, Double, Double> {
val h = (hue * 6).toInt()
val f = hue * 6 - h
val p = value * (1 - saturation)
val q = value * (1 - f * saturation)
val t = value * (1 - (1 - f) * saturation)
return when (h) {
0 -> Triple(value, t, p)
1 -> Triple(q, value, p)
2 -> Triple(p, value, t)
3 -> Triple(p, q, value)
4 -> Triple(t, p, value)
5 -> Triple(value, p, q)
else -> Triple(0.0, 0.0, 0.0)
}
}
}
inline fun <reified T: ListenableWorker> enqueueOneTimeWork(appCtx: Context, workBuilder: (OneTimeWorkRequest.Builder) -> OneTimeWorkRequest.Builder): LiveData<WorkInfo> {
val work = workBuilder(OneTimeWorkRequestBuilder<T>()).build()
val workManager = WorkManager.getInstance(appCtx)

View File

@@ -59,6 +59,7 @@
<string name="dialog_location_br"><![CDATA[<b>Location:</b><br>%1$s<br>]]></string>
<string name="dialog_time"><![CDATA[<b>Time:</b><br>%1$s - %2$s]]></string>
<string name="dialog_course_br"><![CDATA[<b>Course:</b><br>%1$s<br>]]></string>
<string name="course_type">Course Type</string>
<plurals name="not_course_update_text">
<item quantity="one">One course message</item>
<item quantity="other">%1$d course messages</item>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="FUTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
<style name="FUTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>

View File

@@ -2,6 +2,7 @@
buildscript {
ext.kotlin_version = '1.5.31'
ext.compose_version = '1.0.5'
repositories {
google()
mavenCentral()