Skip to content

Commit 0d43e01

Browse files
authored
Merge pull request #11056 from wordpress-mobile/issue/11012-ia-fab-tooltip
Tooltip onboarding announcement for IA create FAB
2 parents 69cfe28 + 2289fb3 commit 0d43e01

File tree

17 files changed

+404
-11
lines changed

17 files changed

+404
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package org.wordpress.android.ui
2+
3+
import android.animation.Animator
4+
import android.animation.AnimatorListenerAdapter
5+
import android.content.Context
6+
import android.util.AttributeSet
7+
import android.view.Gravity
8+
import android.view.View
9+
import android.widget.LinearLayout
10+
import android.widget.TextView
11+
import androidx.annotation.LayoutRes
12+
import org.wordpress.android.R
13+
import org.wordpress.android.ui.WPTooltipView.TooltipPosition.ABOVE
14+
import org.wordpress.android.ui.WPTooltipView.TooltipPosition.BELOW
15+
import org.wordpress.android.ui.WPTooltipView.TooltipPosition.LEFT
16+
import org.wordpress.android.ui.WPTooltipView.TooltipPosition.RIGHT
17+
import org.wordpress.android.util.RtlUtils
18+
import java.lang.IllegalArgumentException
19+
20+
/**
21+
* Partially based on https://stackoverflow.com/a/42756576
22+
*
23+
* NOTE: this is a very basic implementation of a tooltip component
24+
* mainly used to cover the need of onboarding/announcing in the IA Project main create FAB.
25+
* More work/rework is needed to make it a full custom view tooltip component.
26+
* A different and more dynamic approach that can be used is with PopupWindow taking care of behaviors like
27+
* CoordinatorLayout behavior (as in this scenario with FAB and snackbars)
28+
*/
29+
30+
private const val HIDE_ANIMATION_DURATION = 50L
31+
32+
class WPTooltipView @JvmOverloads constructor (
33+
context: Context,
34+
attrs: AttributeSet? = null,
35+
defStyleAttr: Int = 0
36+
) : LinearLayout(context, attrs, defStyleAttr) {
37+
private var position = LEFT
38+
private var messageId = 0
39+
private var arrowHorizontalOffsetFromEndResId = 0
40+
private var arrowHorizontalOffsetFromStartResId = 0
41+
private var arrowHorizontalOffsetFromEnd = -1
42+
private var arrowHorizontalOffsetFromStart = -1
43+
private var animationDuration: Int
44+
45+
init {
46+
attrs?.also {
47+
val stylesAttributes = context.theme.obtainStyledAttributes(
48+
attrs,
49+
R.styleable.WPTooltipView,
50+
0,
51+
0
52+
)
53+
54+
try {
55+
position = TooltipPosition.fromInt(
56+
stylesAttributes.getInt(R.styleable.WPTooltipView_wpTooltipPosition, 0)
57+
)
58+
messageId = stylesAttributes.getResourceId(R.styleable.WPTooltipView_wpTooltipMessage, 0)
59+
arrowHorizontalOffsetFromEndResId = stylesAttributes.getResourceId(
60+
R.styleable.WPTooltipView_wpArrowHorizontalOffsetFromEnd, 0
61+
)
62+
arrowHorizontalOffsetFromStartResId = stylesAttributes.getResourceId(
63+
R.styleable.WPTooltipView_wpArrowHorizontalOffsetFromStart, 0
64+
)
65+
} finally {
66+
stylesAttributes.recycle()
67+
}
68+
}
69+
70+
inflate(getContext(), position.layout, this)
71+
val root = findViewById<LinearLayout>(R.id.root_view)
72+
val tvMessage = findViewById<TextView>(R.id.tooltip_message)
73+
val arrow = findViewById<View>(R.id.tooltip_arrow)
74+
animationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
75+
76+
if (messageId > 0) {
77+
tvMessage.setText(messageId)
78+
}
79+
80+
if (RtlUtils.isRtl(context)) {
81+
when (position) {
82+
LEFT -> arrow.rotation = -90f
83+
RIGHT -> arrow.rotation = 90f
84+
ABOVE, BELOW -> {}
85+
}
86+
}
87+
88+
if (position == ABOVE || position == BELOW) {
89+
if (arrowHorizontalOffsetFromEndResId > 0) {
90+
arrowHorizontalOffsetFromEnd = resources.getDimensionPixelSize(arrowHorizontalOffsetFromEndResId)
91+
root.gravity = Gravity.END
92+
val lp = arrow.layoutParams as LayoutParams
93+
lp.marginEnd = arrowHorizontalOffsetFromEnd
94+
arrow.layoutParams = lp
95+
} else if (arrowHorizontalOffsetFromStartResId > 0) {
96+
arrowHorizontalOffsetFromStart = resources.getDimensionPixelSize(arrowHorizontalOffsetFromStartResId)
97+
root.gravity = Gravity.START
98+
val lp = arrow.layoutParams as LayoutParams
99+
lp.marginStart = arrowHorizontalOffsetFromStart
100+
arrow.layoutParams = lp
101+
}
102+
}
103+
}
104+
105+
enum class TooltipPosition(val value: Int, @LayoutRes val layout: Int) {
106+
LEFT(0, R.layout.tooltip_left),
107+
RIGHT(1, R.layout.tooltip_right),
108+
ABOVE(2, R.layout.tooltip_above),
109+
BELOW(3, R.layout.tooltip_below);
110+
111+
companion object {
112+
fun fromInt(value: Int): TooltipPosition =
113+
values().firstOrNull { it.value == value }
114+
?: throw IllegalArgumentException("TooltipPosition wrong value $value")
115+
}
116+
}
117+
118+
fun show() {
119+
this.postDelayed({
120+
this.apply {
121+
alpha = 0f
122+
visibility = View.VISIBLE
123+
animate()
124+
.alpha(1f)
125+
.setDuration(animationDuration.toLong())
126+
.setListener(null)
127+
}
128+
}, 400)
129+
}
130+
131+
fun hide() {
132+
this.apply {
133+
animate()
134+
.alpha(0f)
135+
.setDuration(HIDE_ANIMATION_DURATION)
136+
.setListener(object : AnimatorListenerAdapter() {
137+
override fun onAnimationEnd(animation: Animator?) {
138+
super.onAnimationEnd(animation)
139+
visibility = View.GONE
140+
}
141+
})
142+
}
143+
}
144+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.wordpress.android.ui.main
2+
3+
data class MainFabUiState(
4+
val isFabVisible: Boolean,
5+
val isFabTooltipVisible: Boolean
6+
)

WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java

+15-2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import org.wordpress.android.ui.PagePostCreationSourcesDetail;
7575
import org.wordpress.android.ui.RequestCodes;
7676
import org.wordpress.android.ui.ShortcutsNavigator;
77+
import org.wordpress.android.ui.WPTooltipView;
7778
import org.wordpress.android.ui.accounts.LoginActivity;
7879
import org.wordpress.android.ui.accounts.SignupEpilogueActivity;
7980
import org.wordpress.android.ui.main.WPMainNavigationView.OnPageListener;
@@ -172,6 +173,7 @@ public class WPMainActivity extends AppCompatActivity implements
172173
private SiteModel mSelectedSite;
173174
private WPMainActivityViewModel mViewModel;
174175
private FloatingActionButton mFloatingActionButton;
176+
private WPTooltipView mFabTooltip;
175177
private static final String MAIN_BOTTOM_SHEET_TAG = "MAIN_BOTTOM_SHEET_TAG";
176178

177179
@Inject AccountStore mAccountStore;
@@ -386,13 +388,20 @@ public boolean isGooglePlayServicesAvailable(Activity activity) {
386388

387389
private void initViewModel() {
388390
mFloatingActionButton = findViewById(R.id.fab_button);
391+
mFabTooltip = findViewById(R.id.fab_tooltip);
389392

390393
mViewModel = ViewModelProviders.of(this, mViewModelFactory).get(WPMainActivityViewModel.class);
391394

392395
if (BuildConfig.INFORMATION_ARCHITECTURE_AVAILABLE) {
393396
// Setup Observers
394-
mViewModel.getShowMainActionFab().observe(this, shouldShow -> {
395-
if (shouldShow) {
397+
mViewModel.getFabUiState().observe(this, fabUiState -> {
398+
if (fabUiState.isFabTooltipVisible()) {
399+
mFabTooltip.show();
400+
} else {
401+
mFabTooltip.hide();
402+
}
403+
404+
if (fabUiState.isFabVisible()) {
396405
mFloatingActionButton.show();
397406
} else {
398407
mFloatingActionButton.hide();
@@ -414,6 +423,10 @@ private void initViewModel() {
414423
mViewModel.setIsBottomSheetShowing(true);
415424
});
416425

426+
mFabTooltip.setOnClickListener(v -> {
427+
mViewModel.onTooltipTapped();
428+
});
429+
417430
mViewModel.isBottomSheetShowing().observe(this, event -> {
418431
event.applyIfNotHandled(isShowing -> {
419432
FragmentManager fm = getSupportFragmentManager();

WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java

+13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import androidx.annotation.NonNull;
88

9+
import org.wordpress.android.BuildConfig;
910
import org.wordpress.android.WordPress;
1011
import org.wordpress.android.analytics.AnalyticsTracker;
1112
import org.wordpress.android.analytics.AnalyticsTracker.Stat;
@@ -204,6 +205,9 @@ public enum UndeletablePrefKey implements PrefKey {
204205

205206
// Used to indicate whether or not the the post-signup interstitial must be shown
206207
SHOULD_SHOW_POST_SIGNUP_INTERSTITIAL,
208+
209+
// used to indicate that we do not need to show the main FAB tooltip
210+
IS_MAIN_FAB_TOOLTIP_DISABLED,
207211
}
208212

209213
private static SharedPreferences prefs() {
@@ -893,6 +897,15 @@ public static boolean isQuickStartDisabled() {
893897
return getBoolean(UndeletablePrefKey.IS_QUICK_START_DISABLED, false);
894898
}
895899

900+
public static void setMainFabTooltipDisabled(Boolean disable) {
901+
setBoolean(UndeletablePrefKey.IS_MAIN_FAB_TOOLTIP_DISABLED, disable);
902+
}
903+
904+
public static boolean isMainFabTooltipDisabled() {
905+
return !BuildConfig.INFORMATION_ARCHITECTURE_AVAILABLE
906+
|| getBoolean(UndeletablePrefKey.IS_MAIN_FAB_TOOLTIP_DISABLED, false);
907+
}
908+
896909
public static void setQuickStartMigrationDialogShown(Boolean shown) {
897910
setBoolean(UndeletablePrefKey.HAS_QUICK_START_MIGRATION_SHOWN, shown);
898911
}

WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt

+4
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ class AppPrefsWrapper @Inject constructor() {
109109

110110
fun removeAppWidgetHasData(appWidgetId: Int) = AppPrefs.removeStatsWidgetHasData(appWidgetId)
111111

112+
fun setMainFabTooltipDisabled(disable: Boolean) = AppPrefs.setMainFabTooltipDisabled(disable)
113+
114+
fun isMainFabTooltipDisabled() = AppPrefs.isMainFabTooltipDisabled()
115+
112116
companion object {
113117
private const val LIGHT_MODE_ID = 0
114118
private const val DARK_MODE_ID = 1

WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt

+30-5
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ import org.wordpress.android.ui.main.MainActionListItem.ActionType
99
import org.wordpress.android.ui.main.MainActionListItem.ActionType.CREATE_NEW_PAGE
1010
import org.wordpress.android.ui.main.MainActionListItem.ActionType.CREATE_NEW_POST
1111
import org.wordpress.android.ui.main.MainActionListItem.CreateAction
12+
import org.wordpress.android.ui.main.MainFabUiState
13+
import org.wordpress.android.ui.prefs.AppPrefsWrapper
1214
import org.wordpress.android.viewmodel.Event
1315
import org.wordpress.android.viewmodel.SingleLiveEvent
1416
import javax.inject.Inject
1517

16-
class WPMainActivityViewModel @Inject constructor() : ViewModel() {
18+
class WPMainActivityViewModel @Inject constructor(private val appPrefsWrapper: AppPrefsWrapper) : ViewModel() {
1719
private var isStarted = false
1820

19-
private val _showMainActionFab = MutableLiveData<Boolean>()
20-
val showMainActionFab: LiveData<Boolean> = _showMainActionFab
21+
private val _fabUiState = MutableLiveData<MainFabUiState>()
22+
val fabUiState: LiveData<MainFabUiState> = _fabUiState
2123

2224
private val _mainActions = MutableLiveData<List<MainActionListItem>>()
2325
val mainActions: LiveData<List<MainActionListItem>> = _mainActions
@@ -32,7 +34,8 @@ class WPMainActivityViewModel @Inject constructor() : ViewModel() {
3234
if (isStarted) return
3335
isStarted = true
3436

35-
_showMainActionFab.value = isFabVisible
37+
setMainFabUiState(isFabVisible)
38+
3639
loadMainActions()
3740
}
3841

@@ -61,10 +64,32 @@ class WPMainActivityViewModel @Inject constructor() : ViewModel() {
6164
}
6265

6366
fun setIsBottomSheetShowing(showing: Boolean) {
67+
appPrefsWrapper.setMainFabTooltipDisabled(true)
68+
setMainFabUiState(true)
69+
6470
_isBottomSheetShowing.value = Event(showing)
6571
}
6672

6773
fun onPageChanged(showFab: Boolean) {
68-
_showMainActionFab.value = showFab
74+
setMainFabUiState(showFab)
75+
}
76+
77+
fun onTooltipTapped() {
78+
val oldState = _fabUiState.value
79+
oldState?.let {
80+
_fabUiState.value = MainFabUiState(
81+
isFabVisible = it.isFabVisible,
82+
isFabTooltipVisible = false
83+
)
84+
}
85+
}
86+
87+
private fun setMainFabUiState(isFabVisible: Boolean) {
88+
val newState = MainFabUiState(
89+
isFabVisible = isFabVisible,
90+
isFabTooltipVisible = if (appPrefsWrapper.isMainFabTooltipDisabled()) false else isFabVisible
91+
)
92+
93+
_fabUiState.value = newState
6994
}
7095
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
3+
<item android:id="@android:id/background">
4+
<shape>
5+
<solid android:color="@color/transparent"/>
6+
</shape>
7+
</item>
8+
<item>
9+
<rotate android:fromDegrees="45" android:toDegrees="45" android:pivotX="-40%" android:pivotY="87%" >
10+
<shape android:shape="rectangle" >
11+
<stroke android:color="@color/background_snackbar" android:width="10dp"/>
12+
<solid android:color="@color/background_snackbar" />
13+
</shape>
14+
</rotate>
15+
</item>
16+
</layer-list>

WordPress/src/main/res/layout/main_activity.xml

+14-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@
5656
app:menu="@menu/bottom_nav_main"/>
5757
</LinearLayout>
5858

59+
<org.wordpress.android.ui.WPTooltipView
60+
android:id="@+id/fab_tooltip"
61+
android:layout_width="wrap_content"
62+
android:layout_height="wrap_content"
63+
app:wpTooltipPosition="above"
64+
android:visibility="gone"
65+
tools:visibility="visible"
66+
app:wpTooltipMessage="@string/create_post_page_fab_tooltip"
67+
app:wpArrowHorizontalOffsetFromEnd="@dimen/main_fab_tooltip_offset_end"
68+
android:layout_marginStart="@dimen/margin_medium"
69+
android:layout_alignParentEnd="true"
70+
android:layout_marginEnd="@dimen/margin_medium"
71+
android:layout_above="@+id/fab_button"/>
72+
5973
<com.google.android.material.floatingactionbutton.FloatingActionButton
6074
android:id="@+id/fab_button"
6175
android:layout_width="wrap_content"
@@ -70,5 +84,4 @@
7084
android:src="@drawable/ic_create_white_24dp"
7185
app:borderWidth="0dp"/>
7286

73-
7487
</RelativeLayout>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools"
4+
android:id="@+id/root_view"
5+
style="@style/TooltipContainer"
6+
android:gravity="center_horizontal"
7+
android:orientation="vertical">
8+
9+
<TextView
10+
android:id="@+id/tooltip_message"
11+
style="@style/TooltipTextView"
12+
android:layout_weight="1"
13+
tools:text="This is an example tooltip message" />
14+
15+
<View
16+
android:id="@+id/tooltip_arrow"
17+
style="@style/TooltipArrowView"
18+
android:layout_weight="1"
19+
android:rotation="-180"
20+
android:background="@drawable/tooltip_arrow"
21+
android:contentDescription="@null"/>
22+
23+
</LinearLayout>

0 commit comments

Comments
 (0)