diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryActivity.java index e1f7ce55101..e3f9785d327 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryActivity.java @@ -99,6 +99,8 @@ import org.odk.collect.android.external.InstancesContract; import org.odk.collect.android.formentry.BackgroundAudioPermissionDialogFragment; import org.odk.collect.android.formentry.BackgroundAudioViewModel; +import org.odk.collect.android.formentry.FormAnimation; +import org.odk.collect.android.formentry.FormAnimationType; import org.odk.collect.android.formentry.FormEndView; import org.odk.collect.android.formentry.FormEntryMenuDelegate; import org.odk.collect.android.formentry.FormEntryViewModel; @@ -138,7 +140,7 @@ import org.odk.collect.android.listeners.AdvanceToNextListener; import org.odk.collect.android.listeners.FormLoaderListener; import org.odk.collect.android.listeners.SavePointListener; -import org.odk.collect.android.listeners.SwipeHandler; +import org.odk.collect.android.formentry.SwipeHandler; import org.odk.collect.android.listeners.WidgetValueChangedListener; import org.odk.collect.android.logic.ImmutableDisplayableQuestion; import org.odk.collect.android.projects.CurrentProjectProvider; @@ -297,10 +299,6 @@ public void allowSwiping(boolean doSwipe) { swipeHandler.setAllowSwiping(doSwipe); } - enum AnimationType { - LEFT, RIGHT, FADE - } - private boolean showNavigationButtons; @Inject @@ -1438,7 +1436,7 @@ public void onScreenRefresh() { int event = getFormController().getEvent(); SwipeHandler.View current = createView(event, false); - showView(current, AnimationType.FADE); + showView(current, FormAnimationType.FADE); formIndexAnimationHandler.setLastIndex(getFormController().getFormIndex()); } @@ -1449,12 +1447,12 @@ private void animateToNextView(int event) { case FormEntryController.EVENT_GROUP: // create a savepoint nonblockingCreateSavePointData(); - showView(createView(event, true), AnimationType.RIGHT); + showView(createView(event, true), FormAnimationType.RIGHT); break; case FormEntryController.EVENT_END_OF_FORM: case FormEntryController.EVENT_REPEAT: case EVENT_PROMPT_NEW_REPEAT: - showView(createView(event, true), AnimationType.RIGHT); + showView(createView(event, true), FormAnimationType.RIGHT); break; case FormEntryController.EVENT_REPEAT_JUNCTURE: Timber.i("Repeat juncture: %s", getFormController().getFormIndex().getReference()); @@ -1468,7 +1466,7 @@ private void animateToNextView(int event) { private void animateToPreviousView(int event) { SwipeHandler.View next = createView(event, false); - showView(next, AnimationType.LEFT); + showView(next, FormAnimationType.LEFT); } /** @@ -1476,7 +1474,7 @@ private void animateToPreviousView(int event) { * current view and next appropriately given the AnimationType. Also updates * the progress bar. */ - public void showView(SwipeHandler.View next, AnimationType from) { + public void showView(SwipeHandler.View next, FormAnimationType from) { invalidateOptionsMenu(); // disable notifications... @@ -1489,7 +1487,7 @@ public void showView(SwipeHandler.View next, AnimationType from) { // logging of the view being shown is already done, as this was handled // by createView() - switch (from) { + switch (FormAnimation.getAnimationTypeBasedOnLanguageDirection(this, from)) { case RIGHT: inAnimation = loadAnimation(this, R.anim.push_left_in); diff --git a/collect_app/src/main/java/org/odk/collect/android/adapters/AbstractSelectListAdapter.java b/collect_app/src/main/java/org/odk/collect/android/adapters/AbstractSelectListAdapter.java index 277117495ec..e50671234aa 100644 --- a/collect_app/src/main/java/org/odk/collect/android/adapters/AbstractSelectListAdapter.java +++ b/collect_app/src/main/java/org/odk/collect/android/adapters/AbstractSelectListAdapter.java @@ -47,6 +47,7 @@ import org.odk.collect.android.utilities.Appearances; import org.odk.collect.audioclips.Clip; import org.odk.collect.imageloader.GlideImageLoader; +import org.odk.collect.strings.localization.LocalizedApplicationKt; import java.io.File; import java.util.ArrayList; @@ -58,7 +59,6 @@ import static org.odk.collect.android.formentry.media.FormMediaUtils.getClip; import static org.odk.collect.android.formentry.media.FormMediaUtils.getClipID; import static org.odk.collect.android.formentry.media.FormMediaUtils.getPlayableAudioURI; -import static org.odk.collect.android.widgets.QuestionWidget.isRTL; public abstract class AbstractSelectListAdapter extends RecyclerView.Adapter implements Filterable { @@ -137,8 +137,8 @@ void setUpButton(TextView button, int index) { button.setTextSize(TypedValue.COMPLEX_UNIT_DIP, QuestionFontSizeUtils.getQuestionFontSize()); button.setText(HtmlUtils.textToHtml(prompt.getSelectChoiceText(filteredItems.get(index)))); button.setTag(items.indexOf(filteredItems.get(index))); - button.setGravity(isRTL() ? Gravity.END : Gravity.START); - button.setTextAlignment(isRTL() ? View.TEXT_ALIGNMENT_TEXT_END : View.TEXT_ALIGNMENT_TEXT_START); + button.setGravity(LocalizedApplicationKt.isLTR(context) ? Gravity.START : Gravity.END); + button.setTextAlignment(LocalizedApplicationKt.isLTR(context) ? View.TEXT_ALIGNMENT_TEXT_START : View.TEXT_ALIGNMENT_TEXT_END); } boolean isItemSelected(List selectedItems, @NonNull Selection item) { diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/FormAnimation.kt b/collect_app/src/main/java/org/odk/collect/android/formentry/FormAnimation.kt new file mode 100644 index 00000000000..0e170c76ece --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/FormAnimation.kt @@ -0,0 +1,23 @@ +package org.odk.collect.android.formentry + +import android.content.Context +import org.odk.collect.strings.localization.isLTR + +enum class FormAnimationType { + LEFT, RIGHT, FADE +} + +object FormAnimation { + @JvmStatic + fun getAnimationTypeBasedOnLanguageDirection(context: Context, formAnimationType: FormAnimationType): FormAnimationType { + return if (context.isLTR()) { + formAnimationType + } else { + when (formAnimationType) { + FormAnimationType.LEFT -> FormAnimationType.RIGHT + FormAnimationType.RIGHT -> FormAnimationType.LEFT + else -> FormAnimationType.FADE + } + } + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEndView.java b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEndView.java index fed2413b9bb..95068c251ff 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEndView.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEndView.java @@ -15,7 +15,6 @@ import androidx.core.widget.NestedScrollView; import org.odk.collect.android.R; -import org.odk.collect.android.listeners.SwipeHandler; import org.odk.collect.android.utilities.FormNameUtils; public class FormEndView extends SwipeHandler.View { diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java b/collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java index bf9b900b8f1..13b4e28145d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java @@ -63,7 +63,6 @@ import org.odk.collect.android.formentry.media.PromptAutoplayer; import org.odk.collect.android.formentry.questions.QuestionTextSizeHelper; import org.odk.collect.android.javarosawrapper.FormController; -import org.odk.collect.android.listeners.SwipeHandler; import org.odk.collect.android.listeners.WidgetValueChangedListener; import org.odk.collect.android.utilities.ContentUriHelper; import org.odk.collect.android.utilities.ExternalAppIntentProvider; diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/SwipeHandler.kt b/collect_app/src/main/java/org/odk/collect/android/formentry/SwipeHandler.kt new file mode 100644 index 00000000000..e9ed0a7869d --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/SwipeHandler.kt @@ -0,0 +1,133 @@ +package org.odk.collect.android.formentry + +import android.content.Context +import android.view.GestureDetector +import android.view.MotionEvent +import android.widget.FrameLayout +import androidx.core.widget.NestedScrollView +import org.odk.collect.android.utilities.FlingRegister +import org.odk.collect.androidshared.utils.ScreenUtils +import org.odk.collect.settings.keys.ProjectKeys +import org.odk.collect.shared.settings.Settings +import org.odk.collect.strings.localization.isLTR +import kotlin.math.abs +import kotlin.math.atan2 + +class SwipeHandler(context: Context, generalSettings: Settings) { + val gestureDetector: GestureDetector + private val onSwipe: OnSwipeListener + private var view: View? = null + private var allowSwiping = true + private var beenSwiped = false + private val generalSettings: Settings + + interface OnSwipeListener { + fun onSwipeBackward() + fun onSwipeForward() + } + + init { + gestureDetector = GestureDetector(context, GestureListener()) + onSwipe = context as OnSwipeListener + this.generalSettings = generalSettings + } + + fun setView(view: View?) { + this.view = view + } + + fun setAllowSwiping(allowSwiping: Boolean) { + this.allowSwiping = allowSwiping + } + + fun setBeenSwiped(beenSwiped: Boolean) { + this.beenSwiped = beenSwiped + } + + fun beenSwiped() = beenSwiped + + inner class GestureListener : GestureDetector.OnGestureListener { + override fun onDown(event: MotionEvent) = false + override fun onSingleTapUp(e: MotionEvent) = false + + override fun onShowPress(e: MotionEvent) = Unit + override fun onLongPress(e: MotionEvent) = Unit + + override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { + // The onFling() captures the 'up' event so our view thinks it gets long pressed. We don't want that, so cancel it. + view?.cancelLongPress() + return false + } + + override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { + if (view == null) { + return false + } + + FlingRegister.flingDetected() + + if (generalSettings.getString(ProjectKeys.KEY_NAVIGATION)!!.contains(ProjectKeys.NAVIGATION_SWIPE) && allowSwiping) { + // Looks for user swipes. If the user has swiped, move to the appropriate screen. + + // For all screens a swipe is left/right of at least .25" and up/down of less than .25" OR left/right of > .5" + val xpixellimit = (ScreenUtils.xdpi(view!!.context) * .25).toInt() + val ypixellimit = (ScreenUtils.ydpi(view!!.context) * .25).toInt() + + if (view != null && view!!.shouldSuppressFlingGesture()) { + return false + } + + if (beenSwiped) { + return false + } + + val diffX = abs(e1.x - e2.x) + val diffY = abs(e1.y - e2.y) + + if (view != null && canScrollVertically() && getGestureAngle(diffX, diffY) > 30) { + return false + } + + if (diffX > xpixellimit && diffY < ypixellimit || diffX > xpixellimit * 2) { + beenSwiped = true + if (e1.x > e2.x) { + if (view!!.context.isLTR()) { + onSwipe.onSwipeForward() + } else { + onSwipe.onSwipeBackward() + } + } else { + if (view!!.context.isLTR()) { + onSwipe.onSwipeBackward() + } else { + onSwipe.onSwipeForward() + } + } + return true + } + } + return false + } + + private fun getGestureAngle(diffX: Float, diffY: Float): Double { + return Math.toDegrees(atan2(diffY.toDouble(), diffX.toDouble())) + } + + private fun canScrollVertically(): Boolean { + val scrollView = view!!.verticalScrollView + + return if (scrollView != null) { + val screenHeight = scrollView.height + val viewHeight = scrollView.getChildAt(0).height + viewHeight > screenHeight + } else { + false + } + } + } + + abstract class View(context: Context) : FrameLayout(context) { + abstract fun shouldSuppressFlingGesture(): Boolean + abstract val verticalScrollView: NestedScrollView? + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/listeners/SwipeHandler.java b/collect_app/src/main/java/org/odk/collect/android/listeners/SwipeHandler.java deleted file mode 100644 index b6aec8fbadc..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/listeners/SwipeHandler.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.odk.collect.android.listeners; - -import android.content.Context; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.widget.NestedScrollView; - -import org.odk.collect.android.utilities.FlingRegister; -import org.odk.collect.androidshared.utils.ScreenUtils; -import org.odk.collect.settings.keys.ProjectKeys; -import org.odk.collect.shared.settings.Settings; - -public class SwipeHandler { - - private final GestureDetector gestureDetector; - private final OnSwipeListener onSwipe; - private View view; - private boolean allowSwiping = true; - private boolean beenSwiped; - private final Settings generalSettings; - - public interface OnSwipeListener { - void onSwipeBackward(); - void onSwipeForward(); - } - - public SwipeHandler(Context context, Settings generalSettings) { - gestureDetector = new GestureDetector(context, new GestureListener()); - this.onSwipe = (OnSwipeListener) context; - this.generalSettings = generalSettings; - } - - public void setView(View view) { - this.view = view; - } - - public void setAllowSwiping(boolean allowSwiping) { - this.allowSwiping = allowSwiping; - } - - public void setBeenSwiped(boolean beenSwiped) { - this.beenSwiped = beenSwiped; - } - - public boolean beenSwiped() { - return beenSwiped; - } - - public GestureDetector getGestureDetector() { - return gestureDetector; - } - - public class GestureListener implements GestureDetector.OnGestureListener { - - @Override - public boolean onDown(MotionEvent event) { - return false; - } - - @Override - public void onShowPress(MotionEvent e) { - - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - // The onFling() captures the 'up' event so our view thinks it gets long pressed. We don't want that, so cancel it. - if (view != null) { - view.cancelLongPress(); - } - return false; - } - - @Override - public void onLongPress(MotionEvent e) { - - } - - @Override - public boolean onSingleTapUp(MotionEvent e) { - return false; - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - if (view == null) { - return false; - } - - FlingRegister.flingDetected(); - - if (e1 != null && e2 != null - && generalSettings.getString(ProjectKeys.KEY_NAVIGATION).contains(ProjectKeys.NAVIGATION_SWIPE) - && allowSwiping) { - // Looks for user swipes. If the user has swiped, move to the appropriate screen. - - // For all screens a swipe is left/right of at least .25" and up/down of less than .25" OR left/right of > .5" - int xpixellimit = (int) (ScreenUtils.xdpi(view.getContext()) * .25); - int ypixellimit = (int) (ScreenUtils.ydpi(view.getContext()) * .25); - - if (view != null && view.shouldSuppressFlingGesture()) { - return false; - } - - if (beenSwiped) { - return false; - } - - float diffX = Math.abs(e1.getX() - e2.getX()); - float diffY = Math.abs(e1.getY() - e2.getY()); - - if (view != null && canScrollVertically() && getGestureAngle(diffX, diffY) > 30) { - return false; - } - - if ((diffX > xpixellimit && diffY < ypixellimit) || diffX > xpixellimit * 2) { - beenSwiped = true; - if (e1.getX() > e2.getX()) { - onSwipe.onSwipeForward(); - } else { - onSwipe.onSwipeBackward(); - } - return true; - } - } - - return false; - } - - private double getGestureAngle(float diffX, float diffY) { - return Math.toDegrees(Math.atan2(diffY, diffX)); - } - - public boolean canScrollVertically() { - NestedScrollView scrollView = view.getVerticalScrollView(); - - if (scrollView != null) { - int screenHeight = scrollView.getHeight(); - int viewHeight = scrollView.getChildAt(0).getHeight(); - return viewHeight > screenHeight; - } else { - return false; - } - } - } - - public abstract static class View extends FrameLayout { - public View(@NonNull Context context) { - super(context); - } - - public abstract boolean shouldSuppressFlingGesture(); - - @Nullable - public abstract NestedScrollView getVerticalScrollView(); - } -} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java index 29043b4c5bd..72ee21d6f3f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java @@ -58,7 +58,6 @@ import org.odk.collect.settings.keys.ProjectKeys; import java.io.File; -import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import javax.inject.Inject; @@ -257,19 +256,6 @@ private TextView configureGuidanceTextView(TextView guidanceTextView, String gui return guidanceTextView; } - //source::https://stackoverflow.com/questions/18996183/identifying-rtl-language-in-android/23203698#23203698 - public static boolean isRTL() { - return isRTL(Locale.getDefault()); - } - - private static boolean isRTL(Locale locale) { - if (locale.getDisplayName().isEmpty()) { - return false; - } - final int directionality = Character.getDirectionality(locale.getDisplayName().charAt(0)); - return directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC; - } - public TextView getHelpTextView() { return helpTextView; } diff --git a/collect_app/src/main/res/drawable/ic_navigate_back.xml b/collect_app/src/main/res/drawable/ic_navigate_back.xml index 5656f976417..506df3ffe1a 100644 --- a/collect_app/src/main/res/drawable/ic_navigate_back.xml +++ b/collect_app/src/main/res/drawable/ic_navigate_back.xml @@ -3,7 +3,8 @@ android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0" - android:tint="?colorOnSurface"> + android:tint="?colorOnSurface" + android:autoMirrored="true"> - - - - diff --git a/collect_app/src/main/res/drawable/ic_navigate_forward.xml b/collect_app/src/main/res/drawable/ic_navigate_forward.xml index 7a919435dec..5cfe59f1173 100644 --- a/collect_app/src/main/res/drawable/ic_navigate_forward.xml +++ b/collect_app/src/main/res/drawable/ic_navigate_forward.xml @@ -3,7 +3,8 @@ android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0" - android:tint="?colorOnSurface"> + android:tint="?colorOnSurface" + android:autoMirrored="true"> - - - - \ No newline at end of file diff --git a/collect_app/src/main/res/layout/form_entry.xml b/collect_app/src/main/res/layout/form_entry.xml index 1b3a19e1bc6..a1bac2e7ad3 100644 --- a/collect_app/src/main/res/layout/form_entry.xml +++ b/collect_app/src/main/res/layout/form_entry.xml @@ -76,7 +76,7 @@ the License. android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" android:contentDescription="@string/form_backward" - android:drawableStart="@drawable/ic_navigate_back_wrapped" + android:drawableStart="@drawable/ic_navigate_back" android:focusable="true" android:padding="10dp" android:text="@string/form_backward" @@ -98,7 +98,7 @@ the License. android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" android:contentDescription="@string/form_forward" - android:drawableEnd="@drawable/ic_navigate_forward_wrapped" + android:drawableEnd="@drawable/ic_navigate_forward" android:focusable="true" android:padding="10dp" android:text="@string/form_forward" diff --git a/geo/src/main/res/drawable/ic_distance_wrapped.xml b/geo/src/main/res/drawable/ic_distance_wrapped.xml deleted file mode 100644 index 8a5b6973008..00000000000 --- a/geo/src/main/res/drawable/ic_distance_wrapped.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/geo/src/main/res/layout-land/geopoly_layout.xml b/geo/src/main/res/layout-land/geopoly_layout.xml index 250fb0d5fa2..9d6c0b60d8f 100644 --- a/geo/src/main/res/layout-land/geopoly_layout.xml +++ b/geo/src/main/res/layout-land/geopoly_layout.xml @@ -35,7 +35,7 @@ android:layout_marginStart="@dimen/margin_standard" android:layout_marginTop="@dimen/margin_standard" android:layout_marginEnd="@dimen/margin_standard" - android:drawableEnd="@drawable/ic_distance_wrapped" + android:drawableEnd="@drawable/ic_distance" android:padding="15dp" android:paddingBottom="20dp" android:text="@string/record_geopoint" diff --git a/geo/src/main/res/layout/geopoly_layout.xml b/geo/src/main/res/layout/geopoly_layout.xml index a762a0a2c12..e747703cfc2 100644 --- a/geo/src/main/res/layout/geopoly_layout.xml +++ b/geo/src/main/res/layout/geopoly_layout.xml @@ -35,7 +35,7 @@ android:layout_marginStart="@dimen/margin_standard" android:layout_marginTop="@dimen/margin_standard" android:layout_marginEnd="@dimen/margin_standard" - android:drawableEnd="@drawable/ic_distance_wrapped" + android:drawableEnd="@drawable/ic_distance" android:padding="15dp" android:paddingBottom="20dp" android:text="@string/record_geopoint" diff --git a/strings/src/main/java/org/odk/collect/strings/localization/LocalizedApplication.kt b/strings/src/main/java/org/odk/collect/strings/localization/LocalizedApplication.kt index 371514093de..243ed611212 100644 --- a/strings/src/main/java/org/odk/collect/strings/localization/LocalizedApplication.kt +++ b/strings/src/main/java/org/odk/collect/strings/localization/LocalizedApplication.kt @@ -3,6 +3,7 @@ package org.odk.collect.strings.localization import android.content.Context import android.content.res.Configuration import android.os.Build +import android.view.View import java.util.Locale interface LocalizedApplication { @@ -27,3 +28,7 @@ fun Context.getLocalizedString(stringId: Int, vararg formatArgs: Any): String { .resources .getString(stringId, *formatArgs) } + +fun Context.isLTR(): Boolean { + return resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR +}