summaryrefslogtreecommitdiffhomepage
path: root/android/src
diff options
context:
space:
mode:
Diffstat (limited to 'android/src')
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt202
-rw-r--r--android/src/main/res/drawable/cell_switch_background.xml27
-rw-r--r--android/src/main/res/layout/preferences.xml5
-rw-r--r--android/src/main/res/values/dimensions.xml6
4 files changed, 240 insertions, 0 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt
new file mode 100644
index 0000000000..622ff38799
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt
@@ -0,0 +1,202 @@
+package net.mullvad.mullvadvpn.ui
+
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Paint.Style
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.OvalShape
+import android.util.AttributeSet
+import android.view.GestureDetector
+import android.view.GestureDetector.OnGestureListener
+import android.view.Gravity
+import android.view.MotionEvent
+import android.widget.ImageView
+import android.widget.LinearLayout
+import net.mullvad.mullvadvpn.R
+
+class CellSwitch : LinearLayout {
+ enum class State {
+ ON,
+ OFF
+ }
+
+ var state = State.OFF
+ set(value) {
+ if (field != value) {
+ field = value
+ animateToState()
+ }
+ }
+
+ private val onColor = resources.getColor(R.color.green)
+ private val offColor = resources.getColor(R.color.red)
+
+ private val knobSize = resources.getDimensionPixelSize(R.dimen.cell_switch_knob_size)
+ private val knobImage = ShapeDrawable(OvalShape()).apply {
+ paint.apply {
+ color = offColor
+ style = Style.FILL
+ }
+
+ intrinsicWidth = knobSize
+ intrinsicHeight = knobSize
+ }
+
+ private val knobView = ImageView(context).apply {
+ setImageDrawable(knobImage)
+ }
+
+ private val knobAnimationDuration = 200L
+ private val knobMaxTranslation =
+ resources.getDimensionPixelOffset(R.dimen.cell_switch_knob_max_translation).toFloat()
+
+ private val knobPosition: Float
+ get() = knobView.translationX / knobMaxTranslation
+
+ private val positionAnimation = ValueAnimator.ofFloat(0f, knobMaxTranslation).apply {
+ addUpdateListener { animation ->
+ knobView.translationX = animation.animatedValue as Float
+ }
+
+ duration = knobAnimationDuration
+ }
+
+ private val colorAnimation = ValueAnimator.ofArgb(offColor, onColor).apply {
+ addUpdateListener { animation ->
+ knobImage.paint.color = animation.animatedValue as Int
+ knobImage.invalidateSelf()
+ }
+
+ duration = knobAnimationDuration
+ }
+
+ private val gestureListener = object : OnGestureListener {
+ private var isScrolling: Boolean = false
+ private var scrollPosition: Float = 0f
+
+ override fun onDown(event: MotionEvent): Boolean {
+ scrollPosition = knobView.translationX
+ return true
+ }
+
+ override fun onFling(
+ downEvent: MotionEvent,
+ upEvent: MotionEvent,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ if (velocityX > 0f) {
+ state = State.ON
+ } else if (velocityX < 0f) {
+ state = State.OFF
+ }
+
+ return true
+ }
+
+ override fun onLongPress(event: MotionEvent) {}
+
+ override fun onScroll(
+ downEvent: MotionEvent,
+ moveEvent: MotionEvent,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+ isScrolling = true
+ scrollPosition -= distanceX
+
+ var fraction = scrollPosition / knobMaxTranslation
+ val playTime = (fraction * knobAnimationDuration).toLong()
+
+ colorAnimation.pause()
+ positionAnimation.pause()
+
+ colorAnimation.currentPlayTime = playTime
+ positionAnimation.currentPlayTime = playTime
+
+ return true
+ }
+
+ override fun onShowPress(event: MotionEvent) {}
+
+ override fun onSingleTapUp(event: MotionEvent): Boolean {
+ when (state) {
+ State.ON -> state = State.OFF
+ State.OFF -> state = State.ON
+ }
+
+ return true
+ }
+
+ fun onUp(): Boolean {
+ if (!isScrolling) {
+ return false
+ }
+
+ if (knobPosition <= 0.5f) {
+ state = State.OFF
+ } else {
+ state = State.ON
+ }
+
+ isScrolling = false
+ scrollPosition = 0f
+
+ return true
+ }
+ }
+
+ private val gestureDetector = GestureDetector(context, gestureListener)
+
+ 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 {
+ setBackground(resources.getDrawable(R.drawable.cell_switch_background, null))
+ addView(knobView, LinearLayout.LayoutParams(knobSize, knobSize).apply {
+ gravity = Gravity.CENTER_VERTICAL
+ leftMargin = resources.getDimensionPixelSize(R.dimen.cell_switch_knob_margin)
+ })
+ }
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ if (gestureDetector.onTouchEvent(event)) {
+ return true
+ } else if (event.actionMasked == MotionEvent.ACTION_UP) {
+ return gestureListener.onUp()
+ }
+
+ return super.onTouchEvent(event)
+ }
+
+ private fun animateToState() {
+ var playTime = (knobPosition * knobAnimationDuration).toLong()
+
+ when (state) {
+ State.ON -> {
+ colorAnimation.start()
+ positionAnimation.start()
+ }
+ State.OFF -> {
+ colorAnimation.reverse()
+ positionAnimation.reverse()
+
+ playTime = knobAnimationDuration - playTime
+ }
+ }
+
+ colorAnimation.currentPlayTime = playTime
+ positionAnimation.currentPlayTime = playTime
+ }
+}
diff --git a/android/src/main/res/drawable/cell_switch_background.xml b/android/src/main/res/drawable/cell_switch_background.xml
new file mode 100644
index 0000000000..71f6fe0802
--- /dev/null
+++ b/android/src/main/res/drawable/cell_switch_background.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle"
+ >
+ <item android:state_enabled="false">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/cell_switch_border_radius"/>
+ <stroke android:color="@color/white20" android:width="2dp"/>
+ <size
+ android:width="@dimen/cell_switch_width"
+ android:height="@dimen/cell_switch_height"
+ />
+ </shape>
+ </item>
+
+ <item android:state_enabled="true">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/cell_switch_border_radius"/>
+ <stroke android:color="@color/white" android:width="2dp"/>
+ <size
+ android:width="@dimen/cell_switch_width"
+ android:height="@dimen/cell_switch_height"
+ />
+ </shape>
+ </item>
+</selector>
diff --git a/android/src/main/res/layout/preferences.xml b/android/src/main/res/layout/preferences.xml
index 38089ce7e4..870a9a5206 100644
--- a/android/src/main/res/layout/preferences.xml
+++ b/android/src/main/res/layout/preferences.xml
@@ -60,6 +60,11 @@
android:textStyle="bold"
android:text="@string/local_network_sharing"
/>
+ <net.mullvad.mullvadvpn.ui.CellSwitch
+ android:layout_width="52dp"
+ android:layout_height="32dp"
+ android:layout_weight="0"
+ />
</LinearLayout>
<TextView android:id="@+id/allow_lan_footer"
android:layout_width="match_parent"
diff --git a/android/src/main/res/values/dimensions.xml b/android/src/main/res/values/dimensions.xml
index 800e1ed0ac..5d106210ea 100644
--- a/android/src/main/res/values/dimensions.xml
+++ b/android/src/main/res/values/dimensions.xml
@@ -6,4 +6,10 @@
<dimen name="account_input_corner_radius">4dp</dimen>
<dimen name="normal_button_height">44dp</dimen>
<dimen name="tall_button_height">64dp</dimen>
+ <dimen name="cell_switch_border_radius">16dp</dimen>
+ <dimen name="cell_switch_width">32dp</dimen>
+ <dimen name="cell_switch_height">52dp</dimen>
+ <dimen name="cell_switch_knob_margin">4dp</dimen>
+ <dimen name="cell_switch_knob_max_translation">20dp</dimen>
+ <dimen name="cell_switch_knob_size">24dp</dimen>
</resources>