Skip to content

Commit 795e089

Browse files
authored
Merge branch 'trunk' into release/20.8
2 parents f29077c + 66d79d1 commit 795e089

File tree

107 files changed

+1566
-241
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+1566
-241
lines changed

RELEASE-NOTES.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
20.9
44
-----
5-
5+
* [***] [internal] Redesigned the landing screen of the WordPress and Jetpack apps, behind a feature flag. [https://github.com/wordpress-mobile/WordPress-Android/pull/17203]
6+
* [**] [internal] Added sensor feedback to influence the speed of the scrolling large text on the redesigned Jetpack landing screen based on the vertical rotation of the device. [https://github.com/wordpress-mobile/WordPress-Android/pull/17207]
67

78
20.8
89
-----

WordPress/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ dependencies {
387387
implementation "androidx.compose.material:material:$composeVersion"
388388
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion"
389389
implementation "androidx.compose.ui:ui-tooling-preview:$composeVersion"
390+
implementation "androidx.compose.runtime:runtime-livedata:$composeVersion"
390391
debugImplementation "androidx.compose.ui:ui-tooling:$composeVersion"
391392

392393
testImplementation "junit:junit:$jUnitVersion"

WordPress/src/androidTest/java/org/wordpress/android/e2e/flows/LoginFlow.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ public LoginFlow chooseContinueWithWpCom() {
3838
public LoginFlow enterEmailAddress(String emailAddress) {
3939
// Email Address Screen – Fill it in and click "Continue"
4040
// See LoginEmailFragment
41-
clickOn(R.id.input);
4241
populateTextField(R.id.input, emailAddress);
4342
clickOn(R.id.login_continue_button);
4443
return this;
@@ -47,7 +46,6 @@ public LoginFlow enterEmailAddress(String emailAddress) {
4746
public LoginFlow enterPassword(String password) {
4847
// Password Screen – Fill it in and click "Continue"
4948
// See LoginEmailPasswordFragment
50-
clickOn(R.id.input);
5149
populateTextField(R.id.input, password);
5250
clickOn(R.id.bottom_button);
5351
return this;
@@ -106,9 +104,7 @@ public LoginFlow enterUsernameAndPassword(String username, String password) {
106104
Matchers.instanceOf(EditText.class)));
107105
ViewInteraction passwordElement = onView(allOf(isDescendantOfA(withId(R.id.login_password_row)),
108106
Matchers.instanceOf(EditText.class)));
109-
clickOn(usernameElement);
110107
populateTextField(usernameElement, username + "\n");
111-
clickOn(passwordElement);
112108
populateTextField(passwordElement, password + "\n");
113109
clickOn(R.id.bottom_button);
114110
return this;
@@ -124,7 +120,6 @@ public LoginFlow chooseEnterYourSiteAddress() {
124120
public LoginFlow enterSiteAddress(String siteAddress) {
125121
// Site Address Screen – Fill it in and click "Continue"
126122
// See LoginSiteAddressFragment
127-
clickOn(R.id.input);
128123
populateTextField(R.id.input, siteAddress);
129124
clickOn(R.id.bottom_button);
130125
return this;

WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueFragment.kt

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@ import android.content.Context
44
import android.os.Bundle
55
import android.view.View
66
import androidx.fragment.app.Fragment
7-
import androidx.lifecycle.ViewModelProvider
7+
import androidx.fragment.app.viewModels
88
import com.google.android.material.button.MaterialButton
9+
import dagger.hilt.android.AndroidEntryPoint
910
import org.wordpress.android.R
10-
import org.wordpress.android.WordPress
1111
import org.wordpress.android.databinding.JetpackLoginPrologueScreenBinding
1212
import org.wordpress.android.ui.accounts.LoginNavigationEvents.ShowEmailLoginScreen
1313
import org.wordpress.android.ui.accounts.LoginNavigationEvents.ShowLoginViaSiteAddressScreen
1414
import org.wordpress.android.util.WPActivityUtils
15-
import javax.inject.Inject
1615

16+
@AndroidEntryPoint
1717
class LoginPrologueFragment : Fragment(R.layout.jetpack_login_prologue_screen) {
18-
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
19-
private lateinit var viewModel: LoginPrologueViewModel
18+
private val viewModel: LoginPrologueViewModel by viewModels()
2019
private lateinit var loginPrologueListener: LoginPrologueListener
2120

2221
@Suppress("TooGenericExceptionThrown")
@@ -30,7 +29,6 @@ class LoginPrologueFragment : Fragment(R.layout.jetpack_login_prologue_screen) {
3029

3130
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
3231
super.onViewCreated(view, savedInstanceState)
33-
initDagger()
3432

3533
// setting up a full screen flags for the decor view of this fragment,
3634
// that will work with transparent status bar
@@ -41,14 +39,7 @@ class LoginPrologueFragment : Fragment(R.layout.jetpack_login_prologue_screen) {
4139
with(JetpackLoginPrologueScreenBinding.bind(view)) { initViewModel() }
4240
}
4341

44-
private fun initDagger() {
45-
(requireActivity().application as WordPress).component().inject(this)
46-
}
47-
4842
private fun JetpackLoginPrologueScreenBinding.initViewModel() {
49-
viewModel = ViewModelProvider(this@LoginPrologueFragment, viewModelFactory).get(
50-
LoginPrologueViewModel::class.java
51-
)
5243
initObservers()
5344
viewModel.start()
5445
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package org.wordpress.android.ui.accounts.login
2+
3+
import android.content.Context
4+
import android.content.res.Configuration.UI_MODE_NIGHT_YES
5+
import android.os.Bundle
6+
import android.view.LayoutInflater
7+
import android.view.ViewGroup
8+
import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
9+
import androidx.compose.foundation.layout.Box
10+
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.foundation.layout.size
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.CompositionLocalProvider
14+
import androidx.compose.runtime.LaunchedEffect
15+
import androidx.compose.runtime.compositionLocalOf
16+
import androidx.compose.runtime.livedata.observeAsState
17+
import androidx.compose.runtime.withFrameNanos
18+
import androidx.compose.ui.Alignment
19+
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.platform.ComposeView
21+
import androidx.compose.ui.tooling.preview.Devices
22+
import androidx.compose.ui.tooling.preview.Preview
23+
import androidx.compose.ui.unit.dp
24+
import androidx.fragment.app.Fragment
25+
import androidx.lifecycle.viewmodel.compose.viewModel
26+
import dagger.hilt.android.AndroidEntryPoint
27+
import kotlinx.coroutines.isActive
28+
import org.wordpress.android.ui.accounts.login.components.ColumnWithFrostedGlassBackground
29+
import org.wordpress.android.ui.accounts.login.components.JetpackLogo
30+
import org.wordpress.android.ui.accounts.login.components.LoopingTextWithBackground
31+
import org.wordpress.android.ui.accounts.login.components.PrimaryButton
32+
import org.wordpress.android.ui.accounts.login.components.SecondaryButton
33+
import org.wordpress.android.ui.accounts.login.components.TopLinearGradient
34+
import org.wordpress.android.ui.compose.theme.AppTheme
35+
36+
val LocalPosition = compositionLocalOf { 0f }
37+
38+
@AndroidEntryPoint
39+
class LoginPrologueRevampedFragment : Fragment() {
40+
private lateinit var loginPrologueListener: LoginPrologueListener
41+
42+
override fun onCreateView(
43+
inflater: LayoutInflater,
44+
container: ViewGroup?,
45+
savedInstanceState: Bundle?
46+
) = ComposeView(requireContext()).apply {
47+
setContent {
48+
AppTheme {
49+
PositionProvider {
50+
LoginScreenRevamped(
51+
onWpComLoginClicked = loginPrologueListener::showEmailLoginScreen,
52+
onSiteAddressLoginClicked = loginPrologueListener::loginViaSiteAddress,
53+
)
54+
}
55+
}
56+
}
57+
}
58+
59+
override fun onAttach(context: Context) {
60+
super.onAttach(context)
61+
check(context is LoginPrologueListener) { "$context must implement LoginPrologueListener" }
62+
loginPrologueListener = context
63+
}
64+
65+
override fun onResume() {
66+
super.onResume()
67+
requireActivity().window.addFlags(FLAG_LAYOUT_NO_LIMITS)
68+
}
69+
70+
override fun onPause() {
71+
super.onPause()
72+
requireActivity().window.clearFlags(FLAG_LAYOUT_NO_LIMITS)
73+
}
74+
75+
companion object {
76+
const val TAG = "login_prologue_revamped_fragment_tag"
77+
}
78+
}
79+
80+
/**
81+
* This composable launches an effect to continuously update the view model by providing the elapsed
82+
* time between frames. Velocity and position are recalculated for each frame, with the resulting
83+
* position provided here to be consumed by nested children composables.
84+
*/
85+
@Composable
86+
private fun PositionProvider(
87+
viewModel: LoginPrologueRevampedViewModel = viewModel(),
88+
content: @Composable () -> Unit
89+
) {
90+
val position = viewModel.positionData.observeAsState(0f)
91+
CompositionLocalProvider(LocalPosition provides position.value) {
92+
LaunchedEffect(Unit) {
93+
var lastFrameNanos: Long? = null
94+
while (isActive) {
95+
val currentFrameNanos = withFrameNanos { it }
96+
// Calculate elapsed time (in seconds) since the last frame
97+
val elapsed = (currentFrameNanos - (lastFrameNanos ?: currentFrameNanos)) / 1e9.toFloat()
98+
// Update viewModel for frame
99+
viewModel.updateForFrame(elapsed)
100+
// Update frame timestamp reference
101+
lastFrameNanos = currentFrameNanos
102+
}
103+
}
104+
105+
content()
106+
}
107+
}
108+
109+
@Composable
110+
private fun LoginScreenRevamped(
111+
onWpComLoginClicked: () -> Unit,
112+
onSiteAddressLoginClicked: () -> Unit,
113+
) {
114+
Box {
115+
LoopingTextWithBackground()
116+
TopLinearGradient()
117+
JetpackLogo(
118+
modifier = Modifier
119+
.padding(top = 60.dp)
120+
.size(60.dp)
121+
.align(Alignment.TopCenter)
122+
)
123+
ColumnWithFrostedGlassBackground(
124+
background = { modifier, textModifier -> LoopingTextWithBackground(modifier, textModifier) }
125+
) {
126+
PrimaryButton(onClick = onWpComLoginClicked)
127+
SecondaryButton(onClick = onSiteAddressLoginClicked)
128+
}
129+
}
130+
}
131+
132+
@Preview(showBackground = true, device = Devices.PIXEL_4_XL)
133+
@Preview(showBackground = true, device = Devices.PIXEL_4_XL, uiMode = UI_MODE_NIGHT_YES)
134+
@Composable
135+
fun PreviewLoginScreenRevamped() {
136+
AppTheme {
137+
LoginScreenRevamped(
138+
onWpComLoginClicked = {},
139+
onSiteAddressLoginClicked = {}
140+
)
141+
}
142+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.wordpress.android.ui.accounts.login
2+
3+
import android.content.Context
4+
import android.hardware.Sensor
5+
import android.hardware.SensorEvent
6+
import android.hardware.SensorEventListener
7+
import android.hardware.SensorManager
8+
import androidx.lifecycle.LiveData
9+
import androidx.lifecycle.MutableLiveData
10+
import androidx.lifecycle.ViewModel
11+
import dagger.hilt.android.lifecycle.HiltViewModel
12+
import dagger.hilt.android.qualifiers.ApplicationContext
13+
import javax.inject.Inject
14+
import kotlin.math.exp
15+
import kotlin.math.ln
16+
17+
// This factor is used to convert the raw values emitted from device sensor to an appropriate scale for the consuming
18+
// composables.
19+
private const val ACCELERATION_FACTOR = -0.1f
20+
21+
// The maximum velocity (in either direction)
22+
private const val MAXIMUM_VELOCITY = 0.125f
23+
24+
// The velocity decay factor (i.e. 1/4th velocity after 1 second)
25+
private val VELOCITY_DECAY = -ln(4f)
26+
27+
// An additional acceleration applied to make the text scroll when the device is flat on a table
28+
private const val DRIFT = -0.05f
29+
30+
@HiltViewModel
31+
class LoginPrologueRevampedViewModel @Inject constructor(
32+
@ApplicationContext appContext: Context,
33+
) : ViewModel() {
34+
private var acceleration = -0.3f // Default value when sensor is off
35+
private var velocity = 0f
36+
private var position = 0f
37+
38+
/**
39+
* This function updates the physics model for the interactive animation by applying the elapsed time (in seconds)
40+
* to update the velocity and position.
41+
*
42+
* * Velocity is constrained so that it does not fall below -MAXIMUM_VELOCITY and does not exceed MAXIMUM_VELOCITY.
43+
* * Position is constrained so that it always falls between 0 and 1, and represents the relative vertical offset
44+
* in terms of the height of the repeated child composable.
45+
*
46+
* @param elapsed the elapsed time (in seconds) since the last frame
47+
*/
48+
fun updateForFrame(elapsed: Float) {
49+
// Update the velocity, (decayed and clamped to the maximum)
50+
velocity = (velocity * exp(elapsed * VELOCITY_DECAY) + elapsed * acceleration)
51+
.coerceIn(-MAXIMUM_VELOCITY, MAXIMUM_VELOCITY)
52+
// Update the position, modulo 1 (ensuring a value greater or equal to 0, and less than 1)
53+
position = ((position + elapsed * velocity) % 1 + 1) % 1
54+
55+
_positionData.postValue(position)
56+
}
57+
58+
/** This LiveData responds to accelerometer data from the y-axis of the device and emits updated position data. */
59+
private val _positionData = object : MutableLiveData<Float>(), SensorEventListener {
60+
private val sensorManager
61+
get() = appContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager
62+
63+
override fun onActive() {
64+
super.onActive()
65+
val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
66+
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME)
67+
}
68+
69+
override fun onInactive() {
70+
super.onInactive()
71+
sensorManager.unregisterListener(this)
72+
}
73+
74+
override fun onSensorChanged(event: SensorEvent?) {
75+
event?.values?.let { (_, yAxisAcceleration, _) ->
76+
acceleration = yAxisAcceleration * ACCELERATION_FACTOR + DRIFT
77+
postValue(position)
78+
}
79+
}
80+
81+
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit
82+
}
83+
val positionData: LiveData<Float> = _positionData
84+
}

WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueViewModel.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.wordpress.android.ui.accounts.login
33
import androidx.lifecycle.LiveData
44
import androidx.lifecycle.MediatorLiveData
55
import androidx.lifecycle.MutableLiveData
6+
import dagger.hilt.android.lifecycle.HiltViewModel
67
import kotlinx.coroutines.CoroutineDispatcher
78
import org.wordpress.android.R
89
import org.wordpress.android.analytics.AnalyticsTracker.Stat
@@ -24,6 +25,7 @@ import org.wordpress.android.viewmodel.ScopedViewModel
2425
import javax.inject.Inject
2526
import javax.inject.Named
2627

28+
@HiltViewModel
2729
class LoginPrologueViewModel @Inject constructor(
2830
private val unifiedLoginTracker: UnifiedLoginTracker,
2931
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper,

0 commit comments

Comments
 (0)