diff options
author | Torsten Grote <t@grobox.de> | 2020-02-20 15:31:18 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-02-20 15:32:56 -0300 |
commit | 99b760a29415349a99fd8db4354c8a81c713226a (patch) | |
tree | 9227753f0220b60235efe4bd10315859a5b7c773 | |
parent | 4734bd6e19efc7cc37d138c8c63850cb36a596ba (diff) | |
download | merchant-terminal-android-99b760a29415349a99fd8db4354c8a81c713226a.tar.gz merchant-terminal-android-99b760a29415349a99fd8db4354c8a81c713226a.tar.bz2 merchant-terminal-android-99b760a29415349a99fd8db4354c8a81c713226a.zip |
Allow editing order with -1 and +1 buttons
-rw-r--r-- | app/build.gradle | 9 | ||||
-rw-r--r-- | app/src/main/java/net/taler/merchantpos/Utils.kt | 33 | ||||
-rw-r--r-- | app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt | 2 | ||||
-rw-r--r-- | app/src/main/java/net/taler/merchantpos/order/Definitions.kt | 8 | ||||
-rw-r--r-- | app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt | 8 | ||||
-rw-r--r-- | app/src/main/java/net/taler/merchantpos/order/OrderManager.kt | 33 | ||||
-rw-r--r-- | app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt | 75 | ||||
-rw-r--r-- | app/src/main/res/drawable/selectable_background.xml | 5 | ||||
-rw-r--r-- | app/src/main/res/layout/fragment_order.xml | 24 | ||||
-rw-r--r-- | app/src/main/res/layout/list_item_order.xml | 15 | ||||
-rw-r--r-- | app/src/main/res/navigation/nav_graph.xml | 7 | ||||
-rw-r--r-- | app/src/main/res/values-night/colors.xml | 4 | ||||
-rw-r--r-- | app/src/main/res/values-v21/styles.xml | 7 | ||||
-rw-r--r-- | app/src/main/res/values/colors.xml | 1 | ||||
-rw-r--r-- | app/src/main/res/values/styles.xml | 9 |
15 files changed, 215 insertions, 25 deletions
diff --git a/app/build.gradle b/app/build.gradle index 4eea459..ca16c9d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ android { buildToolsVersion "29.0.2" defaultConfig { applicationId "net.taler.merchantpos" - minSdkVersion 27 + minSdkVersion 21 targetSdkVersion 29 versionCode 1 versionName "1.0" @@ -38,9 +38,10 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.2.0' - implementation 'com.google.android.material:material:1.1.0-rc02' + implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "androidx.recyclerview:recyclerview:1.1.0" + implementation "androidx.recyclerview:recyclerview-selection:1.1.0-rc01" // Navigation def nav_version = "2.2.1" @@ -61,9 +62,9 @@ dependencies { implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" // JSON parsing and serialization - implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.7" + implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2" - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } diff --git a/app/src/main/java/net/taler/merchantpos/Utils.kt b/app/src/main/java/net/taler/merchantpos/Utils.kt index 5ebb0b4..a318d23 100644 --- a/app/src/main/java/net/taler/merchantpos/Utils.kt +++ b/app/src/main/java/net/taler/merchantpos/Utils.kt @@ -1,5 +1,9 @@ package net.taler.merchantpos +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.Observer + object Utils { private const val HEX_CHARS = "0123456789ABCDEF" @@ -34,3 +38,32 @@ object Utils { } } + +class CombinedLiveData<T, K, S>( + source1: LiveData<T>, + source2: LiveData<K>, + private val combine: (data1: T?, data2: K?) -> S +) : MediatorLiveData<S>() { + + private var data1: T? = null + private var data2: K? = null + + init { + super.addSource(source1) { t -> + data1 = t + value = combine(data1, data2) + } + super.addSource(source2) { k -> + data2 = k + value = combine(data1, data2) + } + } + + override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) { + throw UnsupportedOperationException() + } + + override fun <T : Any?> removeSource(toRemote: LiveData<T>) { + throw UnsupportedOperationException() + } +} diff --git a/app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt b/app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt index 4d387da..4520f8f 100644 --- a/app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt +++ b/app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt @@ -32,7 +32,7 @@ class ConfigFetcherFragment : Fragment() { when { result == null -> return@Observer result.error -> onNetworkError(result.authError) - else -> findNavController().navigate(R.id.order) + else -> findNavController().navigate(R.id.action_configFetcher_to_order) } }) } diff --git a/app/src/main/java/net/taler/merchantpos/order/Definitions.kt b/app/src/main/java/net/taler/merchantpos/order/Definitions.kt index 57666d7..ce2e464 100644 --- a/app/src/main/java/net/taler/merchantpos/order/Definitions.kt +++ b/app/src/main/java/net/taler/merchantpos/order/Definitions.kt @@ -78,6 +78,14 @@ data class Order(val availableCategories: Map<Int, Category>) { products[product] = (products[product] ?: 0) + 1 return this } + + operator fun minus(product: ConfigProduct): Order { + var quantity = products[product] ?: throw IllegalStateException() + quantity -= 1 + if (quantity > 0) products[product] = quantity + else products.remove(product) + return this + } } typealias OrderLine = Pair<ConfigProduct, Int> diff --git a/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt b/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt index 8856bda..6696afe 100644 --- a/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt +++ b/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.Observer import androidx.navigation.NavController import androidx.navigation.Navigation.findNavController import androidx.navigation.fragment.findNavController +import androidx.transition.TransitionManager.beginDelayedTransition import kotlinx.android.synthetic.main.fragment_order.* import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R @@ -33,6 +34,7 @@ class OrderFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { restartButton.setOnClickListener { orderManager.restartOrUndo() } orderManager.restartState.observe(viewLifecycleOwner, Observer { state -> + beginDelayedTransition(view as ViewGroup) if (state == UNDO) { restartButton.setText(R.string.order_undo) restartButton.isEnabled = true @@ -43,6 +45,12 @@ class OrderFragment : Fragment() { completeButton.isEnabled = state == ENABLED } }) + minusButton.setOnClickListener { orderManager.decreaseSelectedOrderLine() } + plusButton.setOnClickListener { orderManager.increaseSelectedOrderLine() } + orderManager.modifyOrderAllowed.observe(viewLifecycleOwner, Observer { allowed -> + minusButton.isEnabled = allowed + plusButton.isEnabled = allowed + }) } override fun onActivityCreated(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/net/taler/merchantpos/order/OrderManager.kt b/app/src/main/java/net/taler/merchantpos/order/OrderManager.kt index 124e73a..449d741 100644 --- a/app/src/main/java/net/taler/merchantpos/order/OrderManager.kt +++ b/app/src/main/java/net/taler/merchantpos/order/OrderManager.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.Transformations.map import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper import net.taler.merchantpos.Amount.Companion.fromString +import net.taler.merchantpos.CombinedLiveData import net.taler.merchantpos.config.ConfigurationReceiver import net.taler.merchantpos.order.RestartState.DISABLED import net.taler.merchantpos.order.RestartState.ENABLED @@ -40,6 +41,13 @@ class OrderManager(private val mapper: ObjectMapper) : ConfigurationReceiver { private val mRestartState = MutableLiveData<RestartState>().apply { value = DISABLED } internal val restartState: LiveData<RestartState> = mRestartState + private val mSelectedOrderLine = MutableLiveData<OrderLine>() + + internal val modifyOrderAllowed = + CombinedLiveData(restartState, mSelectedOrderLine) { restartState, selectedOrderLine -> + restartState != DISABLED && selectedOrderLine != null + } + @Suppress("BlockingMethodInNonBlockingContext") // run on Dispatchers.Main override suspend fun onConfigurationReceived(json: JSONObject, currency: String): Boolean { // parse categories @@ -105,6 +113,14 @@ class OrderManager(private val mapper: ObjectMapper) : ConfigurationReceiver { } @UiThread + internal fun removeProduct(product: ConfigProduct) { + val order = mOrder.value ?: throw IllegalStateException() + val modifiedOrder = order - product + mOrder.value = modifiedOrder + mRestartState.value = if (modifiedOrder.products.isEmpty()) DISABLED else ENABLED + } + + @UiThread internal fun restartOrUndo() { if (restartState.value == UNDO) { mOrder.value = undoOrder @@ -117,4 +133,21 @@ class OrderManager(private val mapper: ObjectMapper) : ConfigurationReceiver { } } + @UiThread + fun selectOrderLine(orderLine: OrderLine?) { + mSelectedOrderLine.value = orderLine + } + + @UiThread + fun increaseSelectedOrderLine() { + val orderLine = mSelectedOrderLine.value ?: throw IllegalStateException() + addProduct(orderLine.first) + } + + @UiThread + fun decreaseSelectedOrderLine() { + val orderLine = mSelectedOrderLine.value ?: throw IllegalStateException() + removeProduct(orderLine.first) + } + } diff --git a/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt b/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt index 6a0aa2b..240ee3b 100644 --- a/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt +++ b/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt @@ -2,24 +2,35 @@ package net.taler.merchantpos.order import android.os.Bundle import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer +import androidx.recyclerview.selection.ItemDetailsLookup +import androidx.recyclerview.selection.ItemKeyProvider +import androidx.recyclerview.selection.SelectionPredicates +import androidx.recyclerview.selection.SelectionTracker +import androidx.recyclerview.selection.StorageStrategy import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder import kotlinx.android.synthetic.main.fragment_order_state.* import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R +import net.taler.merchantpos.order.OrderAdapter.OrderLineLookup import net.taler.merchantpos.order.OrderAdapter.OrderViewHolder + class OrderStateFragment : Fragment() { private val viewModel: MainViewModel by activityViewModels() private val orderManager by lazy { viewModel.orderManager } private val adapter = OrderAdapter() + private var tracker: SelectionTracker<String>? = null override fun onCreateView( inflater: LayoutInflater, @@ -34,6 +45,26 @@ class OrderStateFragment : Fragment() { adapter = this@OrderStateFragment.adapter layoutManager = LinearLayoutManager(requireContext()) } + val detailsLookup = OrderLineLookup(orderList) + val tracker = SelectionTracker.Builder( + "order-selection-id", + orderList, + adapter.keyProvider, + detailsLookup, + StorageStrategy.createStringStorage() + ).withSelectionPredicate( + SelectionPredicates.createSelectSingleAnything() + ).build() + savedInstanceState?.let { tracker.onRestoreInstanceState(it) } + adapter.tracker = tracker + tracker.addObserver(object : SelectionTracker.SelectionObserver<String>() { + override fun onItemStateChanged(key: String, selected: Boolean) { + super.onItemStateChanged(key, selected) + val item = if (selected) adapter.getItemByKey(key) else null + orderManager.selectOrderLine(item) + } + }) + this.tracker = tracker orderManager.order.observe(viewLifecycleOwner, Observer { order -> adapter.setItems(order.products) @@ -47,10 +78,17 @@ class OrderStateFragment : Fragment() { }) } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + tracker?.onSaveInstanceState(outState) + } + } -private class OrderAdapter : RecyclerView.Adapter<OrderViewHolder>() { +private class OrderAdapter : Adapter<OrderViewHolder>() { + lateinit var tracker: SelectionTracker<String> + val keyProvider = OrderKeyProvider() private val orderLines = ArrayList<OrderLine>() override fun getItemCount() = orderLines.size @@ -62,7 +100,8 @@ private class OrderAdapter : RecyclerView.Adapter<OrderViewHolder>() { } override fun onBindViewHolder(holder: OrderViewHolder, position: Int) { - holder.bind(orderLines[position]) + val item = orderLines[position] + holder.bind(item, tracker.isSelected(item.first.id)) } fun setItems(items: HashMap<ConfigProduct, Int>) { @@ -71,16 +110,44 @@ private class OrderAdapter : RecyclerView.Adapter<OrderViewHolder>() { notifyDataSetChanged() } - private inner class OrderViewHolder(v: View) : RecyclerView.ViewHolder(v) { + fun getItemByKey(key: String): OrderLine? { + return orderLines.find { it.first.id == key } + } + + private inner class OrderViewHolder(private val v: View) : ViewHolder(v) { private val quantity: TextView = v.findViewById(R.id.quantity) private val name: TextView = v.findViewById(R.id.name) private val price: TextView = v.findViewById(R.id.price) - fun bind(orderLine: OrderLine) { + fun bind(orderLine: OrderLine, selected: Boolean) { + v.isActivated = selected quantity.text = orderLine.second.toString() name.text = orderLine.first.description price.text = String.format("%.2f", orderLine.first.priceAsDouble * orderLine.second) } } + private inner class OrderKeyProvider : ItemKeyProvider<String>(SCOPE_MAPPED) { + override fun getKey(position: Int) = orderLines[position].first.id + override fun getPosition(key: String): Int { + return orderLines.indexOfFirst { it.first.id == key } + } + } + + internal class OrderLineLookup(private val list: RecyclerView) : ItemDetailsLookup<String>() { + override fun getItemDetails(e: MotionEvent): ItemDetails<String>? { + list.findChildViewUnder(e.x, e.y)?.let { view -> + val holder = list.getChildViewHolder(view) + val adapter = list.adapter as OrderAdapter + val position = holder.adapterPosition + return object : ItemDetails<String>() { + override fun getPosition(): Int = position + override fun getSelectionKey(): String = adapter.keyProvider.getKey(position) + override fun inSelectionHotspot(e: MotionEvent) = true + } + } + return null + } + } + } diff --git a/app/src/main/res/drawable/selectable_background.xml b/app/src/main/res/drawable/selectable_background.xml new file mode 100644 index 0000000..b82de92 --- /dev/null +++ b/app/src/main/res/drawable/selectable_background.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@color/selectedBackground" android:state_activated="true" /> + <item android:drawable="@android:color/transparent" /> +</selector>
\ No newline at end of file diff --git a/app/src/main/res/layout/fragment_order.xml b/app/src/main/res/layout/fragment_order.xml index bcf8be8..136d1e7 100644 --- a/app/src/main/res/layout/fragment_order.xml +++ b/app/src/main/res/layout/fragment_order.xml @@ -63,6 +63,28 @@ app:layout_constraintStart_toStartOf="parent" /> <Button + android:id="@+id/plusButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:minWidth="48dp" + android:text="+1" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@+id/minusButton" + tools:ignore="HardcodedText" /> + + <Button + android:id="@+id/minusButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:minWidth="48dp" + android:text="-1" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@+id/restartButton" + tools:ignore="HardcodedText" /> + + <Button android:id="@+id/reconfigureButton" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -70,7 +92,7 @@ android:backgroundTint="@color/bottomButtons" android:text="@string/button_reconfigure" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toEndOf="@+id/restartButton" /> + app:layout_constraintStart_toEndOf="@+id/plusButton" /> <Button android:id="@+id/historyButton" diff --git a/app/src/main/res/layout/list_item_order.xml b/app/src/main/res/layout/list_item_order.xml index 5dc2f86..03b15a8 100644 --- a/app/src/main/res/layout/list_item_order.xml +++ b/app/src/main/res/layout/list_item_order.xml @@ -4,9 +4,9 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingStart="8dp" - android:paddingTop="8dp" - android:paddingEnd="8dp"> + android:background="@drawable/selectable_background" + android:minHeight="48dp" + android:padding="8dp"> <TextView android:id="@+id/quantity" @@ -14,8 +14,10 @@ android:layout_height="wrap_content" android:gravity="end" android:minWidth="24dp" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" + app:layout_constraintTop_toTopOf="@+id/name" + app:layout_constraintVertical_bias="0.0" tools:text="31" /> <TextView @@ -24,6 +26,7 @@ android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/price" app:layout_constraintStart_toEndOf="@+id/quantity" app:layout_constraintTop_toTopOf="parent" @@ -33,8 +36,10 @@ android:id="@+id/price" android:layout_width="wrap_content" android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" + app:layout_constraintTop_toTopOf="@+id/name" + app:layout_constraintVertical_bias="0.0" tools:text="23.42" /> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index f615ba2..dc4015f 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -42,7 +42,7 @@ android:id="@+id/merchantSettings" android:name="net.taler.merchantpos.config.MerchantConfigFragment" android:label="Merchant Settings" - tools:layout="@layout/fragment_merchant_settings"/> + tools:layout="@layout/fragment_merchant_settings" /> <fragment android:id="@+id/configFetcher" @@ -53,6 +53,11 @@ android:id="@+id/action_configFetcher_to_merchantSettings" app:destination="@id/merchantSettings" app:popUpToInclusive="true" /> + <action + android:id="@+id/action_configFetcher_to_order" + app:destination="@id/order" + app:launchSingleTop="true" + app:popUpTo="@+id/order" /> </fragment> <fragment diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..47721b4 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="selectedBackground">#363636</color> +</resources> diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml deleted file mode 100644 index e546804..0000000 --- a/app/src/main/res/values-v21/styles.xml +++ /dev/null @@ -1,7 +0,0 @@ -<resources> - <style name="AppTheme.NoActionBar"> - <item name="windowActionBar">false</item> - <item name="windowNoTitle">true</item> - <item name="android:statusBarColor">@android:color/transparent</item> - </style> -</resources> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 10354c5..3ed8874 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -4,6 +4,7 @@ <color name="colorPrimaryDark">#5D4037</color> <color name="colorAccent">#FFEB3B</color> + <color name="selectedBackground">#DADADA</color> <color name="bottomButtons">#9E9D24</color> <color name="logoutButton">#C62828</color> </resources> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9eac8b0..4445a01 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -3,14 +3,19 @@ <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> + <item name="colorOnPrimary">@android:color/white</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style> + <style name="AppTheme.NoActionBar"> <item name="windowActionBar">false</item> <item name="windowNoTitle">true</item> + <item name="android:statusBarColor">@android:color/transparent</item> </style> - <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/> - <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/> + + <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> + + <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" /> </resources> |