diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-09-02 10:37:47 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-09-02 10:37:47 -0300 |
| commit | e01c04bd2c8c9ad3d09b449d5cd0a74bb6275948 (patch) | |
| tree | fd65f3cfe6ee8f4f2dd737539fa9052b0cb53334 | |
| parent | b66faec8a39beaf9ed54b4545462abd983ef7fff (diff) | |
| parent | 8e01b0d90c130cedac792c110c8a42ef19768b57 (diff) | |
| download | mullvadvpn-e01c04bd2c8c9ad3d09b449d5cd0a74bb6275948.tar.xz mullvadvpn-e01c04bd2c8c9ad3d09b449d5cd0a74bb6275948.zip | |
Merge branch 'create-account-input-widget'
6 files changed, 308 insertions, 244 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInput.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInput.kt deleted file mode 100644 index 76ac2a86cf..0000000000 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInput.kt +++ /dev/null @@ -1,215 +0,0 @@ -package net.mullvad.mullvadvpn.ui - -import android.content.Context -import android.text.Editable -import android.text.TextWatcher -import android.text.style.MetricAffectingSpan -import android.view.MotionEvent -import android.view.View -import android.view.View.OnTouchListener -import android.widget.ArrayAdapter -import android.widget.ImageButton -import android.widget.ListView -import android.widget.TextView -import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.ui.AccountInputContainer.BorderState - -const val MIN_ACCOUNT_TOKEN_LENGTH = 10 - -class AccountInput(val parentView: View, context: Context) { - private val disabledBackgroundColor = context.getColor(R.color.white20) - private val disabledTextColor = context.getColor(R.color.white) - private val enabledBackgroundColor = context.getColor(R.color.white) - private val enabledTextColor = context.getColor(R.color.blue) - private val errorTextColor = context.getColor(R.color.red) - - private var inputHasFocus = false - set(value) { - field = value - updateBorder() - if (value == true) { - shouldShowAccountHistory = true - } - } - - private var usingErrorColor = false - set(value) { - field = value - updateBorder() - } - - var state = LoginState.Initial - set(value) { - when (value) { - LoginState.Initial -> initialState() - LoginState.InProgress -> loggingInState() - LoginState.Success -> successState() - LoginState.Failure -> failureState() - } - } - - val container: AccountInputContainer = parentView.findViewById(R.id.account_input_container) - val input: TextView = parentView.findViewById(R.id.account_input) - val button: ImageButton = parentView.findViewById(R.id.login_button) - val accountHistoryList: ListView = parentView.findViewById(R.id.account_history_list) - - var accountHistory: ArrayList<String>? = null - set(value) { - synchronized(this) { - field = value - updateAccountHistory() - } - } - - private var shouldShowAccountHistory = false - set(value) { - synchronized(this) { - field = value - updateAccountHistory() - } - } - - var onLogin: ((String) -> Unit)? = null - - init { - button.setOnClickListener { - onLogin?.invoke(input.text.toString()) - } - setButtonEnabled(false) - - input.apply { - addTextChangedListener(InputWatcher()) - setOnTouchListener( - OnTouchListener { - _, event -> - if (MotionEvent.ACTION_UP == event.getAction()) { - shouldShowAccountHistory = true - } - false - } - ) - } - - container.setOnClickListener { shouldShowAccountHistory = true } - } - - private fun initialState() { - setButtonEnabled(input.text.length >= MIN_ACCOUNT_TOKEN_LENGTH) - button.visibility = View.VISIBLE - - input.apply { - setTextColor(enabledTextColor) - setEnabled(true) - visibility = View.VISIBLE - } - } - - private fun loggingInState() { - setButtonEnabled(false) - button.visibility = View.GONE - - input.apply { - setTextColor(disabledTextColor) - setEnabled(false) - visibility = View.VISIBLE - clearFocus() - } - accountHistoryList.visibility = View.INVISIBLE - } - - private fun successState() { - setButtonEnabled(false) - button.visibility = View.GONE - input.visibility = View.GONE - container.visibility = View.INVISIBLE - } - - private fun failureState() { - setButtonEnabled(false) - button.visibility = View.VISIBLE - - input.apply { - findFocus() - setTextColor(errorTextColor) - setEnabled(true) - visibility = View.VISIBLE - } - - usingErrorColor = true - } - - private fun setButtonEnabled(enabled: Boolean) { - button.apply { - if (enabled != isEnabled()) { - setEnabled(enabled) - setClickable(enabled) - setFocusable(enabled) - } - } - } - - private fun updateAccountHistory() { - accountHistory?.let { history -> - accountHistoryList.apply { - setAdapter( - ArrayAdapter( - context, - R.layout.account_history_entry, - R.id.account_history_entry_text_view, - history - ) - ) - - setOnItemClickListener { _, _, idx, _ -> - val accountNumber = history[idx] - - input.setText(accountNumber) - accountHistoryList.visibility = View.GONE - onLogin?.invoke(accountNumber) - } - } - - if (shouldShowAccountHistory && accountHistoryList.visibility != View.VISIBLE) { - accountHistoryList.visibility = View.VISIBLE - accountHistoryList.translationY = -accountHistoryList.height.toFloat() - accountHistoryList.animate().translationY(0.0F).setDuration(350).start() - } - } - } - - private fun updateBorder() { - if (usingErrorColor) { - container.borderState = BorderState.ERROR - } else if (inputHasFocus) { - container.borderState = BorderState.FOCUSED - } else { - container.borderState = BorderState.UNFOCUSED - } - } - - private fun leaveErrorState() { - if (usingErrorColor) { - input.setTextColor(enabledTextColor) - usingErrorColor = false - } - } - - private fun removeFormattingSpans(text: Editable) { - for (span in text.getSpans(0, text.length, MetricAffectingSpan::class.java)) { - text.removeSpan(span) - } - } - - inner class InputWatcher : TextWatcher { - override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {} - - override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {} - - override fun afterTextChanged(text: Editable) { - inputHasFocus = true - removeFormattingSpans(text) - setButtonEnabled(text.length >= MIN_ACCOUNT_TOKEN_LENGTH) - leaveErrorState() - } - } -} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInputController.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInputController.kt new file mode 100644 index 0000000000..4c690537ba --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInputController.kt @@ -0,0 +1,117 @@ +package net.mullvad.mullvadvpn.ui + +import android.content.Context +import android.view.View +import android.widget.ArrayAdapter +import android.widget.ListView +import kotlin.properties.Delegates.observable +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.ui.AccountInputContainer.BorderState +import net.mullvad.mullvadvpn.ui.widget.AccountInput + +class AccountInputController(val parentView: View, context: Context) { + private var inputHasFocus by observable(false) { _, _, hasFocus -> + updateBorder() + + if (hasFocus) { + shouldShowAccountHistory = true + } + } + + var state: LoginState by observable(LoginState.Initial) { _, _, newState -> + input.loginState = newState + + updateBorder() + + when (newState) { + LoginState.Initial -> {} + LoginState.InProgress -> loggingInState() + LoginState.Success -> successState() + LoginState.Failure -> {} + } + } + + val container: AccountInputContainer = parentView.findViewById(R.id.account_input_container) + val accountHistoryList: ListView = parentView.findViewById(R.id.account_history_list) + + val input = parentView.findViewById<AccountInput>(R.id.account_input).apply { + onFocusChanged.subscribe(this) { hasFocus -> + inputHasFocus = hasFocus + } + + onTextChanged.subscribe(this) { _ -> + if (state == LoginState.Failure) { + state = LoginState.Initial + } + } + } + + var accountHistory: ArrayList<String>? = null + set(value) { + synchronized(this) { + field = value + updateAccountHistory() + } + } + + private var shouldShowAccountHistory = false + set(value) { + synchronized(this) { + field = value + updateAccountHistory() + } + } + + var onLogin: ((String) -> Unit)? + get() = input.onLogin + set(value) { input.onLogin = value } + + fun onDestroy() { + input.onFocusChanged.unsubscribe(this) + input.onTextChanged.unsubscribe(this) + } + + private fun loggingInState() { + accountHistoryList.visibility = View.INVISIBLE + } + + private fun successState() { + container.visibility = View.INVISIBLE + } + + private fun updateAccountHistory() { + accountHistory?.let { history -> + accountHistoryList.apply { + setAdapter( + ArrayAdapter( + context, + R.layout.account_history_entry, + R.id.account_history_entry_text_view, + history + ) + ) + + setOnItemClickListener { _, _, idx, _ -> + input.loginWith(history[idx]) + accountHistoryList.visibility = View.GONE + } + } + + if (shouldShowAccountHistory && accountHistoryList.visibility != View.VISIBLE) { + accountHistoryList.visibility = View.VISIBLE + accountHistoryList.translationY = -accountHistoryList.height.toFloat() + accountHistoryList.animate().translationY(0.0F).setDuration(350).start() + } + } + } + + private fun updateBorder() { + if (state == LoginState.Failure) { + container.borderState = BorderState.ERROR + } else if (inputHasFocus) { + container.borderState = BorderState.FOCUSED + } else { + container.borderState = BorderState.UNFOCUSED + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt index 2f3efbf9b0..643e5137cd 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt @@ -28,7 +28,7 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { private lateinit var loggingInStatus: View private lateinit var loggedInStatus: View private lateinit var loginFailStatus: View - private lateinit var accountInput: AccountInput + private lateinit var accountInput: AccountInputController private lateinit var scrollArea: ScrollView private val loggedIn = CompletableDeferred<LoginResult>() @@ -46,7 +46,7 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { loggedInStatus = view.findViewById(R.id.logged_in_status) loginFailStatus = view.findViewById(R.id.login_fail_status) - accountInput = AccountInput(view, parentActivity) + accountInput = AccountInputController(view, parentActivity) accountInput.onLogin = { accountToken -> login(accountToken) } view.findViewById<Button>(R.id.create_account) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountInput.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountInput.kt new file mode 100644 index 0000000000..fd1c5a1e96 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountInput.kt @@ -0,0 +1,160 @@ +package net.mullvad.mullvadvpn.ui.widget + +import android.content.Context +import android.text.Editable +import android.text.TextWatcher +import android.text.style.MetricAffectingSpan +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnFocusChangeListener +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.TextView +import kotlin.properties.Delegates.observable +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.ui.LoginState +import net.mullvad.talpid.util.EventNotifier + +const val MIN_ACCOUNT_TOKEN_LENGTH = 10 + +class AccountInput : LinearLayout { + private val disabledTextColor = context.getColor(R.color.white) + private val enabledTextColor = context.getColor(R.color.blue) + private val errorTextColor = context.getColor(R.color.red) + + private val container = + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE).let { service -> + val inflater = service as LayoutInflater + + inflater.inflate(R.layout.account_input, this) + } + + private val inputWatcher = object : TextWatcher { + override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(text: Editable) { + removeFormattingSpans(text) + setButtonEnabled(text.length >= MIN_ACCOUNT_TOKEN_LENGTH) + onTextChanged.notify(Unit) + } + } + + private val input = container.findViewById<TextView>(R.id.login_input).apply { + addTextChangedListener(inputWatcher) + + onFocusChangeListener = OnFocusChangeListener { view, inputHasFocus -> + hasFocus = inputHasFocus && view.isEnabled + } + } + + private val button = container.findViewById<ImageButton>(R.id.login_button).apply { + setOnClickListener { + onLogin?.invoke(input.text.toString()) + } + } + + val onFocusChanged = EventNotifier(false) + private var hasFocus by onFocusChanged.notifiable() + + val onTextChanged = EventNotifier(Unit) + + var loginState by observable(LoginState.Initial) { _, _, state -> + when (state) { + LoginState.Initial -> initialState() + LoginState.InProgress -> loggingInState() + LoginState.Success -> successState() + LoginState.Failure -> failureState() + } + } + + var onLogin: ((String) -> Unit)? = null + + constructor(context: Context) : super(context) {} + + constructor(context: Context, attributes: AttributeSet) : super(context, attributes) {} + + constructor(context: Context, attributes: AttributeSet, defaultStyleAttribute: Int) : + super(context, attributes, defaultStyleAttribute) {} + + constructor( + context: Context, + attributes: AttributeSet, + defaultStyleAttribute: Int, + defaultStyleResource: Int + ) : super(context, attributes, defaultStyleAttribute, defaultStyleResource) { + } + + init { + orientation = HORIZONTAL + + setButtonEnabled(false) + } + + fun loginWith(accountNumber: String) { + input.text = accountNumber + onLogin?.invoke(accountNumber) + } + + private fun initialState() { + input.apply { + setTextColor(enabledTextColor) + setEnabled(true) + setFocusableInTouchMode(true) + visibility = View.VISIBLE + } + + button.visibility = View.VISIBLE + setButtonEnabled(input.text.length >= MIN_ACCOUNT_TOKEN_LENGTH) + } + + private fun loggingInState() { + input.apply { + setTextColor(disabledTextColor) + setEnabled(false) + setFocusable(false) + visibility = View.VISIBLE + } + + button.visibility = View.GONE + setButtonEnabled(false) + } + + private fun successState() { + button.visibility = View.GONE + setButtonEnabled(false) + + input.visibility = View.GONE + } + + private fun failureState() { + button.visibility = View.VISIBLE + setButtonEnabled(false) + + input.apply { + setTextColor(errorTextColor) + setEnabled(true) + setFocusableInTouchMode(true) + visibility = View.VISIBLE + requestFocus() + } + } + + private fun setButtonEnabled(enabled: Boolean) { + button.apply { + if (enabled != isEnabled()) { + setEnabled(enabled) + setClickable(enabled) + setFocusable(enabled) + } + } + } + + private fun removeFormattingSpans(text: Editable) { + for (span in text.getSpans(0, text.length, MetricAffectingSpan::class.java)) { + text.removeSpan(span) + } + } +} diff --git a/android/src/main/res/layout/account_input.xml b/android/src/main/res/layout/account_input.xml new file mode 100644 index 0000000000..1c23583c42 --- /dev/null +++ b/android/src/main/res/layout/account_input.xml @@ -0,0 +1,24 @@ +<merge xmlns:android="http://schemas.android.com/apk/res/android"> + <EditText android:id="@+id/login_input" + android:digits="0123456789" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:paddingHorizontal="12dp" + android:background="@drawable/account_input_background" + android:inputType="number" + android:singleLine="true" + android:imeOptions="flagNoPersonalizedLearning" + android:textCursorDrawable="@drawable/text_input_cursor" + android:hint="@string/login_hint" + android:textColorHint="@color/blue40" + android:textColor="@color/blue" + android:textSize="@dimen/text_medium_plus" + android:textStyle="bold" /> + <ImageButton android:id="@+id/login_button" + android:layout_width="48dp" + android:layout_height="match_parent" + android:layout_weight="0" + android:background="@drawable/login_button_background" + android:src="@drawable/login_button_arrow" /> +</merge> diff --git a/android/src/main/res/layout/login.xml b/android/src/main/res/layout/login.xml index 87e240928c..de5f6d0f2d 100644 --- a/android/src/main/res/layout/login.xml +++ b/android/src/main/res/layout/login.xml @@ -67,33 +67,11 @@ <net.mullvad.mullvadvpn.ui.AccountInputContainer android:id="@+id/account_input_container" android:layout_width="match_parent" android:layout_height="48dp"> - <LinearLayout android:layout_width="match_parent" - android:layout_height="48dp" - android:layout_alignParentTop="true" - android:orientation="horizontal"> - <EditText android:id="@+id/account_input" - android:digits="0123456789" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_weight="1" - android:paddingHorizontal="12dp" - android:background="@drawable/account_input_background" - android:inputType="number" - android:singleLine="true" - android:imeOptions="flagNoPersonalizedLearning" - android:textCursorDrawable="@drawable/text_input_cursor" - android:hint="@string/login_hint" - android:textColorHint="@color/blue40" - android:textColor="@color/blue" - android:textSize="@dimen/text_medium_plus" - android:textStyle="bold" /> - <ImageButton android:id="@+id/login_button" - android:layout_width="48dp" - android:layout_height="match_parent" - android:layout_weight="0" - android:background="@drawable/login_button_background" - android:src="@drawable/login_button_arrow" /> - </LinearLayout> + <net.mullvad.mullvadvpn.ui.widget.AccountInput android:id="@+id/account_input" + android:layout_width="match_parent" + android:layout_height="48dp" + android:layout_alignParentTop="true" + android:orientation="horizontal" /> </net.mullvad.mullvadvpn.ui.AccountInputContainer> <ListView android:id="@+id/account_history_list" android:layout_width="fill_parent" |
