Skip to content

Commit

Permalink
+
Browse files Browse the repository at this point in the history
  • Loading branch information
WilliamKwokX committed Sep 3, 2023
1 parent 6af69be commit fd6bb61
Show file tree
Hide file tree
Showing 40 changed files with 696 additions and 2 deletions.
1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions contacts/src/main/res/layout/fragment_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
tools:listitem="@layout/item_contact"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

Expand Down
7 changes: 5 additions & 2 deletions contacts/src/main/res/layout/item_contact.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,23 @@
android:paddingVertical="8dp"
android:paddingStart="12dp"
android:orientation="horizontal"
android:gravity="center_vertical">
android:gravity="center_vertical"
tools:ignore="RtlSymmetry">

<ImageView
android:id="@+id/img_avatar"
tools:src="@drawable/aki"
android:layout_marginStart="10dp"
android:layout_width="32dp"
android:layout_height="match_parent"/>

<TextView
android:id="@+id/tv"
tools:text="Aki"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
/>
</LinearLayout>

Expand Down
1 change: 1 addition & 0 deletions stopwatch/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
63 changes: 63 additions & 0 deletions stopwatch/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}

android {
namespace 'pers.shawxingkwok.stopwatch'
compileSdk 33

defaultConfig {
applicationId "pers.shawxingkwok.stopwatch"
minSdk 24
targetSdk 33
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
}

dependencies {

implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

implementation "androidx.activity:activity-ktx:1.7.2"

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

//region shawxingkwok: android-util-view
tasks.withType(KotlinCompile).configureEach{
kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
}

dependencies {
implementation("io.github.shawxingkwok:kt-util:1.0.2")
implementation project(":api") // 'io.github.shawxingkwok:android-util-view:[latest]'
}
//endregion
21 changes: 21 additions & 0 deletions stopwatch/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package pers.shawxingkwok.stopwatch

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("pers.shawxingkwok.stopwatch", appContext.packageName)
}
}
23 changes: 23 additions & 0 deletions stopwatch/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name=".App"
android:theme="@style/Theme.AndroidUtilView">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>

<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>

</manifest>
18 changes: 18 additions & 0 deletions stopwatch/src/main/java/pers/shawxingkwok/stopwatch/App.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package pers.shawxingkwok.stopwatch

import android.annotation.SuppressLint
import android.app.Application
import android.content.Context

class App : Application() {
companion object{
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
private set
}

override fun onCreate() {
super.onCreate()
context = applicationContext
}
}
121 changes: 121 additions & 0 deletions stopwatch/src/main/java/pers/shawxingkwok/stopwatch/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package pers.shawxingkwok.stopwatch

import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import pers.shawxingkwok.androidutil.view.onClick
import pers.shawxingkwok.ktutil.updateIf
import pers.shawxingkwok.stopwatch.databinding.ActivityMainBinding
import java.util.*
import kotlin.concurrent.timer

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val vm: MainViewModel by viewModels()
private val adapter = StopwatchAdapter()

@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.rv.adapter = adapter
binding.rv.layoutManager = LinearLayoutManager(this)

// the way of collecting flow is different in Fragment.
vm.duration.onEach {
binding.tvDuration.text = StopwatchUtil.formatDuration(it)
}
.launchIn(lifecycleScope)

vm.intervals.onEach {
val sizeChanged = it.size != adapter.intervals.size
adapter.intervals = it
adapter.update(sizeChanged)
}
.launchIn(lifecycleScope)

var timer: Timer? = null

vm.isRunning.onEach { isRunning ->
when{
!isRunning -> {
timer?.cancel()
timer = null
}
timer == null ->
timer = timer(period = 10) {
vm.addDuration()
vm.updateTopIntervals()
}
// else -> do nothing if recovered after onStop
}

// switch button stop/start
val tv = binding.tvRight

if (isRunning) {
tv.text = "Stop"
tv.setBackgroundResource(R.drawable.circle_dark_red)
tv.setTextColor(StopwatchUtil.lightRed)
} else {
tv.text = "Start"
tv.setBackgroundResource(R.drawable.circle_dark_green)
tv.setTextColor(StopwatchUtil.lightGreen)
}
}
.launchIn(lifecycleScope)

vm.tvLeftState.onEach {(duration, isRunning) ->
val tv = binding.tvLeft
when{
// disabled
duration == 0 -> {
tv.setBackgroundResource(R.drawable.circle_dark_grey)
tv.text = "Lap"
tv.setTextColor(StopwatchUtil.whiteGrey)
tv.isClickable = false
}
// lap
isRunning -> {
tv.setBackgroundResource(R.drawable.circle_light_grey)
tv.text = "Lap"
tv.setTextColor(StopwatchUtil.white)
tv.isClickable = true
// This OnClickListener is variable, which also explains why there is a
// function `setFixedListeners` at last.
tv.onClick { _ ->
vm.insertIntervals()

val layoutManager = binding.rv.layoutManager as LinearLayoutManager
val topVisiblePosition = layoutManager.findFirstVisibleItemPosition()
.updateIf({ it == -1 }) { 0 }
binding.rv.scrollToPosition(topVisiblePosition)
}
}
// reset
else ->{
tv.setBackgroundResource(R.drawable.circle_light_grey)
tv.text = "Reset"
tv.setTextColor(StopwatchUtil.white)
tv.isClickable = true
tv.onClick {
vm.resetDuration()
vm.clearIntervals()
}
}
}
}
.launchIn(lifecycleScope)

binding.tvRight.onClick {
vm.switchIsRunning()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package pers.shawxingkwok.stopwatch

import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update

class MainViewModel : ViewModel() {
private val _duration = MutableStateFlow(0)
private val _intervals = MutableStateFlow(intArrayOf())
private val _isRunning = MutableStateFlow(false)

val duration: StateFlow<Int> = _duration
val intervals: StateFlow<IntArray> = _intervals
val isRunning: StateFlow<Boolean> = _isRunning
val tvLeftState = combine(duration, isRunning){ a, b -> a to b }

fun addDuration(){
_duration.value++
}

fun resetDuration(){
_duration.value = 0
}

fun insertIntervals(){
_intervals.update { intArrayOf(0) + it }
}

fun updateTopIntervals(){
_intervals.value =
if (_intervals.value.none())
intArrayOf(1)
else
_intervals.value.clone().also { it[0]++ }
}

fun clearIntervals(){
_intervals.value = intArrayOf()
}

fun switchIsRunning(){
_isRunning.update { !it }
}
}
Loading

0 comments on commit fd6bb61

Please sign in to comment.