Skip to content

Commit

Permalink
Fix #3651: [RTL] High-fi rotation animation for icon in Topic lesson …
Browse files Browse the repository at this point in the history
…and Hints and Solution (#3652)

* Update selection_interaction_item.xml

* Update selection_interaction_item.xml

* Update return_to_topic_button_item.xml

* Update return_to_topic_button_item.xml

* Fixed rotation animation for icon in Topic lesson and Hints and Solution

* Optimized code

* Update ViewBindingAdaptersTest.kt

* Fixed tests

* Fixed unwanted code changes

* Update accessibility_label_exemptions.textproto

* fixed nit
  • Loading branch information
veena14cs authored Aug 11, 2021
1 parent 630a433 commit 77ff836
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 19 deletions.
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@
android:name=".app.testing.TopicTestActivityForStory"
android:theme="@style/OppiaThemeWithoutActionBar" />
<activity android:name=".app.testing.TopicRevisionTestActivity" />
<activity android:name=".app.testing.ViewBindingAdaptersTestActivity"/>
<activity
android:name=".app.topic.questionplayer.QuestionPlayerActivity"
android:label="@string/question_player_activity_title"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import org.oppia.android.app.testing.TestFontScaleConfigurationUtilActivity
import org.oppia.android.app.testing.TopicRevisionTestActivity
import org.oppia.android.app.testing.TopicTestActivity
import org.oppia.android.app.testing.TopicTestActivityForStory
import org.oppia.android.app.testing.ViewBindingAdaptersTestActivity
import org.oppia.android.app.topic.TopicActivity
import org.oppia.android.app.topic.questionplayer.QuestionPlayerActivity
import org.oppia.android.app.topic.revisioncard.RevisionCardActivity
Expand Down Expand Up @@ -157,6 +158,7 @@ interface ActivityComponent {
fun inject(topicRevisionTestActivity: TopicRevisionTestActivity)
fun inject(topicTestActivity: TopicTestActivity)
fun inject(topicTestActivityForStory: TopicTestActivityForStory)
fun inject(viewBindingAdaptersTestActivity: ViewBindingAdaptersTestActivity)
fun inject(viewEventLogsActivity: ViewEventLogsActivity)
fun inject(viewEventLogsTestActivity: ViewEventLogsTestActivity)
fun inject(walkthroughActivity: WalkthroughActivity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.databinding.BindingAdapter;

/**
* Holds all custom binding adapters that set miscellaneous values.
*/
/** Holds all custom binding adapters that set miscellaneous values. */
public final class ViewBindingAdapters {

/**
Expand Down Expand Up @@ -45,23 +44,24 @@ public static void setLayoutWidth(@NonNull View view, float width) {
)
public static void setRotationAnimation(View view, boolean isClockwise, float angle) {
if (isClockwise) {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, angle);
valueAnimator.setDuration(300);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
view.setRotation((float) animation.getAnimatedValue());
}
});
valueAnimator.start();
startRotationAnimation(view, isRtlLayout(view) ? 360f : 0f, angle);
} else {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(angle, 0f);
valueAnimator.setDuration(300);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
view.setRotation((float) animation.getAnimatedValue());
}
});
valueAnimator.start();
startRotationAnimation(view, angle, isRtlLayout(view) ? 360f : 0f);
}
}

private static void startRotationAnimation(View view, float fromAngle, float toAngle) {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(fromAngle, toAngle);
valueAnimator.setDuration(300);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
view.setRotation((float) animation.getAnimatedValue());
}
});
valueAnimator.start();
}

private static boolean isRtlLayout(View view) {
return ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.oppia.android.app.testing

import android.os.Bundle
import org.oppia.android.R
import org.oppia.android.app.activity.InjectableAppCompatActivity

/** Test activity for ViewBindingAdapters. */
class ViewBindingAdaptersTestActivity : InjectableAppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityComponent.inject(this)
setContentView(R.layout.activity_view_binding_adapters_test)
}
}
20 changes: 20 additions & 0 deletions app/src/main/res/layout/activity_view_binding_adapters_test.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<FrameLayout
android:id="@+id/expand_list_icon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp">

<ImageView
android:id="@+id/test_drop_down_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:padding="8dp"
android:src="@drawable/ic_arrow_drop_down_black_24dp" />
</FrameLayout>
</layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package org.oppia.android.app.databinding

import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.intent.Intents
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import dagger.Component
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.oppia.android.R
import org.oppia.android.app.activity.ActivityComponent
import org.oppia.android.app.application.ActivityComponentFactory
import org.oppia.android.app.application.ApplicationComponent
import org.oppia.android.app.application.ApplicationInjector
import org.oppia.android.app.application.ApplicationInjectorProvider
import org.oppia.android.app.application.ApplicationModule
import org.oppia.android.app.application.ApplicationStartupListenerModule
import org.oppia.android.app.databinding.ViewBindingAdapters.setRotationAnimation
import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
import org.oppia.android.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.testing.ViewBindingAdaptersTestActivity
import org.oppia.android.app.topic.PracticeTabModule
import org.oppia.android.domain.classify.InteractionsModule
import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule
import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule
import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule
import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule
import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule
import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.android.domain.oppialogger.LogStorageModule
import org.oppia.android.domain.oppialogger.loguploader.LogUploadWorkerModule
import org.oppia.android.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.android.domain.platformparameter.PlatformParameterModule
import org.oppia.android.domain.question.QuestionModule
import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.android.testing.TestImageLoaderModule
import org.oppia.android.testing.TestLogReportingModule
import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.ImageParsingModule
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Inject
import javax.inject.Singleton

/** Default value for float comparison. */
private const val TOLERANCE = 1e-5f

/** Tests for [MarginBindingAdapters]. */
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
@Config(application = ViewBindingAdaptersTest.TestApplication::class, qualifiers = "port-xxhdpi")
class ViewBindingAdaptersTest {

@Inject
lateinit var context: Context

@get:Rule
var activityRule: ActivityScenarioRule<ViewBindingAdaptersTestActivity> =
ActivityScenarioRule(
Intent(
ApplicationProvider.getApplicationContext(),
ViewBindingAdaptersTestActivity::class.java
)
)

@Before
fun setUp() {
setUpTestApplicationComponent()
Intents.init()
}

@After
fun tearDown() {
Intents.release()
}

@Config(qualifiers = "port")
@Test
fun testViewBindingAdapters_ltrIsEnabled_antiClockwise_rotationAngleForLtrIsCorrect() {
val imageViewDropDown = activityRule.scenario.runWithActivity {
val imageViewDropDown: ImageView = it.findViewById(R.id.test_drop_down_icon)
setRotationAnimation(
imageViewDropDown,
/* isClockwise= */ false,
/* angle= */ 180f
)
return@runWithActivity imageViewDropDown
}
assertThat(imageViewDropDown.rotation).isWithin(TOLERANCE).of(180f)
}

@Config(qualifiers = "port")
@Test
fun testViewBindingAdapters_ltrIsEnabled_clockwise_rotationAngleForLtrIsCorrect() {
val imageViewDropDown = activityRule.scenario.runWithActivity {
val imageViewDropDown: ImageView = it.findViewById(R.id.test_drop_down_icon)
setRotationAnimation(
imageViewDropDown,
/* isClockwise= */ true,
/* angle= */ 180f
)
return@runWithActivity imageViewDropDown
}
assertThat(imageViewDropDown.rotation).isWithin(TOLERANCE).of(0f)
}

@Config(qualifiers = "port")
@Test
fun testViewBindingAdapters_rtlIsEnabled_clockwise_rotationAngleForRtlIsCorrect() {
val imageViewDropDown = activityRule.scenario.runWithActivity {
val imageViewDropDown: ImageView = it.findViewById(R.id.test_drop_down_icon)
ViewCompat.setLayoutDirection(imageViewDropDown, ViewCompat.LAYOUT_DIRECTION_RTL)
setRotationAnimation(
imageViewDropDown,
/* isClockwise= */ true,
/* angle= */ 180f
)
return@runWithActivity imageViewDropDown
}
assertThat(imageViewDropDown.rotation).isWithin(TOLERANCE).of(360f)
}

@Config(qualifiers = "port")
@Test
fun testViewBindingAdapters_rtlIsEnabled_antiClockwise_rotationAngleForRtlIsCorrect() {
val imageViewDropDown = activityRule.scenario.runWithActivity {
val imageViewDropDown: ImageView = it.findViewById(R.id.test_drop_down_icon)
ViewCompat.setLayoutDirection(imageViewDropDown, ViewCompat.LAYOUT_DIRECTION_RTL)
setRotationAnimation(
imageViewDropDown,
/* isClockwise= */ false,
/* angle= */ 180f
)
return@runWithActivity imageViewDropDown
}
assertThat(imageViewDropDown.rotation).isWithin(TOLERANCE).of(180f)
}

private inline fun <reified V, A : Activity> ActivityScenario<A>.runWithActivity(
crossinline action: (A) -> V
): V {
// Use Mockito to ensure the routine is actually executed before returning the result.
@Suppress("UNCHECKED_CAST") // The unsafe cast is necessary to make the routine generic.
val fakeMock: Consumer<V> = mock(Consumer::class.java) as Consumer<V>
val valueCaptor = ArgumentCaptor.forClass(V::class.java)
onActivity { fakeMock.consume(action(it)) }
verify(fakeMock).consume(valueCaptor.capture())
return valueCaptor.value
}

private fun setUpTestApplicationComponent() {
ApplicationProvider.getApplicationContext<TestApplication>().inject(this)
}

// TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
@Singleton
@Component(
modules = [
RobolectricModule::class,
PlatformParameterModule::class,
TestDispatcherModule::class, ApplicationModule::class,
LoggerModule::class, ContinueModule::class, FractionInputModule::class,
ItemSelectionInputModule::class, MultipleChoiceInputModule::class,
NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class,
DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class,
GcsResourceModule::class, TestImageLoaderModule::class, ImageParsingModule::class,
HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, RatioInputModule::class,
ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class
]
)
/** Create a TestApplicationComponent. */
interface TestApplicationComponent : ApplicationComponent {
/** Build the TestApplicationComponent. */
@Component.Builder
interface Builder : ApplicationComponent.Builder

/** Inject [ViewBindingAdaptersTest] in TestApplicationComponent . */
fun inject(ViewBindingAdaptersTest: ViewBindingAdaptersTest)
}

/**
* Class to override a dependency throughout the test application, instead of overriding the
* dependencies in every test class, we can just do it once by extending the Application class.
*/
class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
private val component: TestApplicationComponent by lazy {
DaggerViewBindingAdaptersTest_TestApplicationComponent.builder()
.setApplication(this)
.build() as TestApplicationComponent
}

/** Inject [ViewBindingAdaptersTest] in TestApplicationComponent . */
fun inject(viewBindingAdapters: ViewBindingAdaptersTest) {
component.inject(viewBindingAdapters)
}

override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
}

override fun getApplicationInjector(): ApplicationInjector = component
}

private interface Consumer<T> {
/** Represents an operation that accepts a single input argument and returns no result. */
fun consume(value: T)
}
}
1 change: 1 addition & 0 deletions scripts/assets/accessibility_label_exemptions.textproto
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TestFontScal
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TopicRevisionTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TopicTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TopicTestActivityForStory"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/ViewBindingAdaptersTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/walkthrough/WalkthroughActivity"
1 change: 1 addition & 0 deletions scripts/assets/test_file_exemptions.textproto
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TestFontSca
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TopicRevisionTestActivity.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TopicRevisionTestActivityPresenter.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TopicTestActivity.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/ViewBindingAdaptersTestActivity.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/topic/EnablePracticeTab.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/topic/PracticeTabModule.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/topic/RouteToConceptCardListener.kt"
Expand Down

0 comments on commit 77ff836

Please sign in to comment.