react-native-navigation 8.1.2 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -94,7 +94,8 @@ dependencies {
94
94
  implementation 'androidx.annotation:annotation:1.2.0'
95
95
  implementation 'com.google.android.material:material:1.2.0-alpha03'
96
96
 
97
- implementation 'com.github.wix-playground:ahbottomnavigation:3.3.0'
97
+ implementation 'com.github.wix-playground:ahbottomnavigation:4.0.0'
98
+ implementation 'com.github.Dimezis:BlurView:version-3.0.0'
98
99
  // implementation project(':AHBottomNavigation')
99
100
  implementation 'com.github.wix-playground:reflow-animator:1.0.6'
100
101
  implementation 'com.github.clans:fab:1.6.4'
@@ -5,11 +5,13 @@ import androidx.annotation.VisibleForTesting
5
5
  enum class RNNToggles {
6
6
  TOP_BAR_COLOR_ANIMATION__PUSH,
7
7
  TOP_BAR_COLOR_ANIMATION__TABS,
8
+ TAB_BAR_TRANSLUCENCE,
8
9
  }
9
10
 
10
11
  private val ToggleDefaults = mapOf(
11
12
  RNNToggles.TOP_BAR_COLOR_ANIMATION__PUSH to false,
12
13
  RNNToggles.TOP_BAR_COLOR_ANIMATION__TABS to false,
14
+ RNNToggles.TAB_BAR_TRANSLUCENCE to false,
13
15
  )
14
16
 
15
17
  object RNNFeatureToggles {
@@ -3,6 +3,7 @@ package com.reactnativenavigation.options;
3
3
  import android.content.Context;
4
4
 
5
5
  import com.reactnativenavigation.options.params.Bool;
6
+ import com.reactnativenavigation.options.params.BottomTabsLayoutStyle;
6
7
  import com.reactnativenavigation.options.params.Fraction;
7
8
  import com.reactnativenavigation.options.params.NullBool;
8
9
  import com.reactnativenavigation.options.params.NullFraction;
@@ -27,6 +28,11 @@ public class BottomTabsOptions {
27
28
  if (json == null) return options;
28
29
 
29
30
  options.backgroundColor = ThemeColour.parse(context, json.optJSONObject("backgroundColor"));
31
+ options.layoutStyle = BottomTabsLayoutStyle.fromString(json.optString("layoutStyle"));
32
+ options.bottomMargin = FractionParser.parse(json, "bottomMargin");
33
+ options.cornerRadius = FractionParser.parse(json, "cornerRadius");
34
+ options.translucent = BoolParser.parse(json, "translucent");
35
+ options.blurRadius = FractionParser.parse(json, "blurRadius");
30
36
  options.currentTabId = TextParser.parse(json, "currentTabId");
31
37
  options.currentTabIndex = NumberParser.parse(json, "currentTabIndex");
32
38
  options.hideOnScroll = BoolParser.parse(json, "hideOnScroll");
@@ -46,6 +52,11 @@ public class BottomTabsOptions {
46
52
  }
47
53
 
48
54
  public ThemeColour backgroundColor = new NullThemeColour();
55
+ public BottomTabsLayoutStyle layoutStyle = BottomTabsLayoutStyle.LAYOUT_STYLE_UNDEFINED;
56
+ public Fraction bottomMargin = new NullFraction();
57
+ public Fraction cornerRadius = new NullFraction();
58
+ public Bool translucent = new NullBool();
59
+ public Fraction blurRadius = new NullFraction();
49
60
  public Bool hideOnScroll = new NullBool();
50
61
  public Bool visible = new NullBool();
51
62
  public Bool drawBehind = new NullBool();
@@ -79,12 +90,21 @@ public class BottomTabsOptions {
79
90
  if (other.shadowOptions.hasValue()) shadowOptions = shadowOptions.copy().mergeWith(other.shadowOptions);
80
91
  if (other.borderColor.hasValue()) borderColor = other.borderColor;
81
92
  if (other.backgroundColor.hasValue()) backgroundColor = other.backgroundColor;
82
-
93
+ if (other.translucent.hasValue()) translucent = other.translucent;
94
+ if (other.blurRadius.hasValue()) blurRadius = other.blurRadius;
95
+ if (other.layoutStyle.hasValue()) layoutStyle = other.layoutStyle;
96
+ if (other.bottomMargin.hasValue()) bottomMargin = other.bottomMargin;
97
+ if (other.cornerRadius.hasValue()) cornerRadius = other.cornerRadius;
83
98
  }
84
99
 
85
100
  void mergeWithDefault(final BottomTabsOptions defaultOptions) {
86
101
  if (!borderColor.hasValue()) borderColor = defaultOptions.borderColor;
87
102
  if (!backgroundColor.hasValue()) backgroundColor = defaultOptions.backgroundColor;
103
+ if (!translucent.hasValue()) translucent = defaultOptions.translucent;
104
+ if (!blurRadius.hasValue()) blurRadius = defaultOptions.blurRadius;
105
+ if (!layoutStyle.hasValue()) layoutStyle = defaultOptions.layoutStyle;
106
+ if (!bottomMargin.hasValue()) bottomMargin = defaultOptions.bottomMargin;
107
+ if (!cornerRadius.hasValue()) cornerRadius = defaultOptions.cornerRadius;
88
108
  if (!currentTabId.hasValue()) currentTabId = defaultOptions.currentTabId;
89
109
  if (!currentTabIndex.hasValue()) currentTabIndex = defaultOptions.currentTabIndex;
90
110
  if (!hideOnScroll.hasValue()) hideOnScroll = defaultOptions.hideOnScroll;
@@ -35,7 +35,12 @@ public enum LayoutDirection {
35
35
  }
36
36
 
37
37
  public int get() {
38
- return direction;
38
+ return switch (direction) {
39
+ case View.LAYOUT_DIRECTION_RTL -> RTL.direction;
40
+ case View.LAYOUT_DIRECTION_LTR -> LTR.direction;
41
+ case View.LAYOUT_DIRECTION_LOCALE -> LOCALE.direction;
42
+ default -> DEFAULT.direction;
43
+ };
39
44
  }
40
45
 
41
46
  public int inverse() {
@@ -0,0 +1,20 @@
1
+ package com.reactnativenavigation.options.params
2
+
3
+ enum class BottomTabsLayoutStyle {
4
+ STRETCH,
5
+ COMPACT,
6
+ LAYOUT_STYLE_UNDEFINED;
7
+
8
+ fun hasValue(): Boolean = (this != LAYOUT_STYLE_UNDEFINED)
9
+
10
+ companion object {
11
+ @JvmStatic
12
+ fun fromString(mode: String?): BottomTabsLayoutStyle {
13
+ return when (mode?.lowercase()) {
14
+ "stretch" -> STRETCH
15
+ "compact" -> COMPACT
16
+ else -> LAYOUT_STYLE_UNDEFINED
17
+ }
18
+ }
19
+ }
20
+ }
@@ -21,4 +21,8 @@ public class ColorUtils {
21
21
  public static int setAlpha(int color, int alpha) {
22
22
  return (color & 0x00FFFFFF) | (alpha << 24);
23
23
  }
24
+
25
+ public static boolean isOpaque(int color) {
26
+ return (color & 0xFF000000) == 0xFF000000;
27
+ }
24
28
  }
@@ -63,7 +63,15 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
63
63
  tabPresenter.onConfigurationChanged(resolveCurrentOptions());
64
64
  }
65
65
 
66
- public BottomTabsController(Activity activity, List<ViewController<?>> tabs, ChildControllersRegistry childRegistry, EventEmitter eventEmitter, ImageLoader imageLoader, String id, Options initialOptions, Presenter presenter, BottomTabsAttacher tabsAttacher, BottomTabsPresenter bottomTabsPresenter, BottomTabPresenter bottomTabPresenter) {
66
+ public BottomTabsController(Activity activity,
67
+ List<ViewController<?>> tabs,
68
+ ChildControllersRegistry childRegistry,
69
+ EventEmitter eventEmitter,
70
+ ImageLoader imageLoader,
71
+ String id, Options initialOptions,
72
+ Presenter presenter,
73
+ BottomTabsAttacher tabsAttacher,
74
+ BottomTabsPresenter bottomTabsPresenter, BottomTabPresenter bottomTabPresenter) {
67
75
  super(activity, childRegistry, id, presenter, initialOptions);
68
76
  this.tabs = tabs;
69
77
  this.eventEmitter = eventEmitter;
@@ -86,15 +94,17 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
86
94
  @Override
87
95
  public BottomTabsLayout createView() {
88
96
  BottomTabsLayout root = new BottomTabsLayout(getActivity());
97
+ root.setTag("RNN.BottomTabsLayoutRoot");
89
98
 
90
99
  this.bottomTabsContainer = createBottomTabsContainer();
91
100
  this.bottomTabs = bottomTabsContainer.getBottomTabs();
92
101
  Options resolveCurrentOptions = resolveCurrentOptions();
93
102
  tabsAttacher.init(root, resolveCurrentOptions);
94
- presenter.bindView(bottomTabsContainer, this);
103
+ presenter.bindView(bottomTabsContainer, root, this);
95
104
  tabPresenter.bindView(bottomTabs);
96
105
  bottomTabs.setOnTabSelectedListener(this);
97
106
  root.addBottomTabsContainer(bottomTabsContainer);
107
+
98
108
  bottomTabs.addItems(createTabs());
99
109
  setInitialTab(resolveCurrentOptions);
100
110
  tabsAttacher.attach();
@@ -3,13 +3,20 @@ package com.reactnativenavigation.viewcontrollers.bottomtabs
3
3
  import android.animation.Animator
4
4
  import android.graphics.Color
5
5
  import android.view.ViewGroup
6
+ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
6
7
  import androidx.annotation.IntRange
7
8
  import androidx.core.view.updateMargins
8
9
  import com.aurelhubert.ahbottomnavigation.AHBottomNavigation.TitleState
10
+ import com.reactnativenavigation.RNNFeatureToggles
11
+ import com.reactnativenavigation.RNNToggles.TAB_BAR_TRANSLUCENCE
9
12
  import com.reactnativenavigation.options.Options
13
+ import com.reactnativenavigation.options.params.BottomTabsLayoutStyle
14
+ import com.reactnativenavigation.options.params.Fraction
15
+ import com.reactnativenavigation.utils.UiUtils
10
16
  import com.reactnativenavigation.viewcontrollers.viewcontroller.ViewController
11
17
  import com.reactnativenavigation.views.bottomtabs.BottomTabs
12
18
  import com.reactnativenavigation.views.bottomtabs.BottomTabsContainer
19
+ import com.reactnativenavigation.views.bottomtabs.BottomTabsLayout
13
20
  import kotlin.math.max
14
21
  import kotlin.math.roundToInt
15
22
 
@@ -20,12 +27,13 @@ class BottomTabsPresenter(
20
27
  ) {
21
28
  private val bottomTabFinder: BottomTabFinder = BottomTabFinder(tabs)
22
29
  private lateinit var bottomTabsContainer: BottomTabsContainer
30
+ private lateinit var bottomTabsLayout: BottomTabsLayout
23
31
  private lateinit var bottomTabs: BottomTabs
24
32
  private lateinit var tabSelector: TabSelector
25
33
  private val defaultTitleState: TitleState
26
34
  get() {
27
35
  for (i in 0 until bottomTabs.itemsCount) {
28
- if (bottomTabs.getItem(i).hasIcon()) return TitleState.SHOW_WHEN_ACTIVE
36
+ if (bottomTabs.getItem(i)?.hasIcon() == true) return TitleState.SHOW_WHEN_ACTIVE
29
37
  }
30
38
  return TitleState.ALWAYS_SHOW
31
39
  }
@@ -34,8 +42,9 @@ class BottomTabsPresenter(
34
42
  this.defaultOptions = defaultOptions
35
43
  }
36
44
 
37
- fun bindView(bottomTabsContainer: BottomTabsContainer, tabSelector: TabSelector) {
45
+ fun bindView(bottomTabsContainer: BottomTabsContainer, bottomTabsLayout: BottomTabsLayout, tabSelector: TabSelector) {
38
46
  this.bottomTabsContainer = bottomTabsContainer
47
+ this.bottomTabsLayout = bottomTabsLayout
39
48
  this.bottomTabs = bottomTabsContainer.bottomTabs
40
49
  this.tabSelector = tabSelector
41
50
  animator.bindView(this.bottomTabs)
@@ -65,14 +74,51 @@ class BottomTabsPresenter(
65
74
 
66
75
  private fun mergeBottomTabsOptions(options: Options, view: ViewController<*>) {
67
76
  val bottomTabsOptions = options.bottomTabsOptions
68
- if (options.layout.direction.hasValue()) bottomTabs.setLayoutDirection(options.layout.direction)
69
- if (bottomTabsOptions.preferLargeIcons.hasValue()) bottomTabs.setPreferLargeIcons(bottomTabsOptions.preferLargeIcons.get())
70
- if (bottomTabsOptions.titleDisplayMode.hasValue()) {
71
- bottomTabs.titleState = bottomTabsOptions.titleDisplayMode.toState()
77
+
78
+ if (bottomTabsOptions.layoutStyle == BottomTabsLayoutStyle.COMPACT) {
79
+ bottomTabs.layoutParams.width = WRAP_CONTENT
80
+ bottomTabs.refresh()
81
+ }
82
+
83
+ if (bottomTabsOptions.bottomMargin.hasValue()) {
84
+ val margin = extractBottomMarginPx(bottomTabsOptions.bottomMargin)
85
+ bottomTabsLayout.setBottomMargin(margin)
86
+ }
87
+
88
+ if (bottomTabsOptions.cornerRadius.hasValue()) {
89
+ val radius = extractCornerRadius(bottomTabsOptions.cornerRadius)
90
+ bottomTabsContainer.setRoundedCorners(radius)
72
91
  }
92
+
93
+ // Keep this before the translucent check below
73
94
  if (bottomTabsOptions.backgroundColor.hasValue()) {
74
95
  bottomTabsContainer.setBackgroundColor(bottomTabsOptions.backgroundColor.get())
75
96
  }
97
+
98
+ if (RNNFeatureToggles.isEnabled(TAB_BAR_TRANSLUCENCE)) {
99
+ if (bottomTabsOptions.translucent.isTrue) {
100
+ if (bottomTabsOptions.blurRadius.hasValue()) {
101
+ bottomTabsContainer.setBlurRadius(bottomTabsOptions.blurRadius.get().toFloat())
102
+ }
103
+ if (bottomTabsOptions.backgroundColor.hasValue()) {
104
+ bottomTabsContainer.setBlurColor(bottomTabsOptions.backgroundColor.get())
105
+ }
106
+ bottomTabsContainer.enableBackgroundBlur()
107
+ } else if (bottomTabsOptions.translucent.isFalse) {
108
+ bottomTabsContainer.disableBackgroundBlur()
109
+ }
110
+ }
111
+
112
+ if (bottomTabsOptions.elevation.hasValue()) {
113
+ bottomTabsContainer.setElevation(bottomTabsOptions.elevation)
114
+ }
115
+
116
+ if (options.layout.direction.hasValue()) bottomTabs.setLayoutDirection(options.layout.direction)
117
+ if (bottomTabsOptions.preferLargeIcons.hasValue()) bottomTabs.setPreferLargeIcons(bottomTabsOptions.preferLargeIcons.get())
118
+ if (bottomTabsOptions.titleDisplayMode.hasValue()) {
119
+ bottomTabs.setTitleState(bottomTabsOptions.titleDisplayMode.toState())
120
+ }
121
+
76
122
  if (bottomTabsOptions.animateTabSelection.hasValue()) {
77
123
  bottomTabs.setAnimateTabSelection(bottomTabsOptions.animateTabSelection.get())
78
124
  }
@@ -88,7 +134,7 @@ class BottomTabsPresenter(
88
134
  if (tabIndex >= 0) tabSelector.selectTab(tabIndex)
89
135
  }
90
136
  if (bottomTabsOptions.hideOnScroll.hasValue()) {
91
- bottomTabs.isBehaviorTranslationEnabled = bottomTabsOptions.hideOnScroll.get()
137
+ bottomTabs.setBehaviorTranslationEnabled(bottomTabsOptions.hideOnScroll.get())
92
138
  }
93
139
 
94
140
  if (bottomTabsOptions.borderColor.hasValue()) {
@@ -140,10 +186,39 @@ class BottomTabsPresenter(
140
186
 
141
187
  private fun applyBottomTabsOptions(options: Options) {
142
188
  val bottomTabsOptions = options.bottomTabsOptions
189
+ if (bottomTabsOptions.layoutStyle == BottomTabsLayoutStyle.COMPACT) {
190
+ bottomTabs.layoutParams.width = WRAP_CONTENT
191
+ }
192
+
193
+ if (bottomTabsOptions.bottomMargin.hasValue()) {
194
+ val margin = extractBottomMarginPx(bottomTabsOptions.bottomMargin)
195
+ bottomTabsLayout.setBottomMargin(margin)
196
+ }
197
+
198
+ if (bottomTabsOptions.cornerRadius.hasValue()) {
199
+ val radius = extractCornerRadius(bottomTabsOptions.cornerRadius)
200
+ bottomTabsContainer.setRoundedCorners(radius)
201
+ } else {
202
+ bottomTabsContainer.clearRoundedCorners()
203
+ }
204
+
205
+ if (RNNFeatureToggles.isEnabled(TAB_BAR_TRANSLUCENCE) && bottomTabsOptions.translucent.isTrue) {
206
+ if (bottomTabsOptions.blurRadius.hasValue()) {
207
+ bottomTabsContainer.setBlurRadius(bottomTabsOptions.blurRadius.get().toFloat())
208
+ }
209
+ if (bottomTabsOptions.backgroundColor.hasValue()) {
210
+ bottomTabsContainer.setBlurColor(bottomTabsOptions.backgroundColor.get())
211
+ }
212
+ bottomTabsContainer.enableBackgroundBlur()
213
+ } else {
214
+ bottomTabsContainer.disableBackgroundBlur()
215
+ bottomTabsContainer.setBackgroundColor(bottomTabsOptions.backgroundColor.get(Color.WHITE)!!)
216
+ }
217
+
143
218
  bottomTabs.setLayoutDirection(options.layout.direction)
144
219
  bottomTabs.setPreferLargeIcons(options.bottomTabsOptions.preferLargeIcons[false])
145
- bottomTabs.titleState = bottomTabsOptions.titleDisplayMode[defaultTitleState]
146
- bottomTabsContainer.setBackgroundColor(bottomTabsOptions.backgroundColor.get(Color.WHITE)!!)
220
+ bottomTabs.setTitleState(bottomTabsOptions.titleDisplayMode[defaultTitleState])
221
+
147
222
  bottomTabs.setAnimateTabSelection(bottomTabsOptions.animateTabSelection.get(true))
148
223
  if (bottomTabsOptions.currentTabIndex.hasValue()) {
149
224
  val tabIndex = bottomTabsOptions.currentTabIndex.get()
@@ -174,6 +249,7 @@ class BottomTabsPresenter(
174
249
  bottomTabs.hideBottomNavigation(false)
175
250
  }
176
251
  }
252
+
177
253
  if (bottomTabsOptions.elevation.hasValue()) {
178
254
  bottomTabsContainer.setElevation(bottomTabsOptions.elevation)
179
255
  }
@@ -203,7 +279,7 @@ class BottomTabsPresenter(
203
279
  } else {
204
280
  bottomTabsContainer.clearShadow()
205
281
  }
206
- bottomTabs.isBehaviorTranslationEnabled = bottomTabsOptions.hideOnScroll[false]
282
+ bottomTabs.setBehaviorTranslationEnabled(bottomTabsOptions.hideOnScroll[false])
207
283
  }
208
284
 
209
285
  fun applyBottomInset(bottomInset: Int) {
@@ -246,7 +322,37 @@ class BottomTabsPresenter(
246
322
 
247
323
  fun onConfigurationChanged(options: Options) {
248
324
  val bottomTabsOptions = options.withDefaultOptions(defaultOptions).bottomTabsOptions
249
- bottomTabs.setBackgroundColor(bottomTabsOptions.backgroundColor.get(Color.WHITE)!!)
325
+
326
+ if (bottomTabsOptions.layoutStyle == BottomTabsLayoutStyle.COMPACT) {
327
+ bottomTabs.layoutParams.width = WRAP_CONTENT
328
+ }
329
+
330
+ if (bottomTabsOptions.bottomMargin.hasValue()) {
331
+ val margin = extractBottomMarginPx(bottomTabsOptions.bottomMargin)
332
+ bottomTabsLayout.setBottomMargin(margin)
333
+ }
334
+
335
+ if (bottomTabsOptions.cornerRadius.hasValue()) {
336
+ val radius = extractCornerRadius(bottomTabsOptions.cornerRadius)
337
+ bottomTabsContainer.setRoundedCorners(radius)
338
+ } else {
339
+ bottomTabsContainer.clearRoundedCorners()
340
+ }
341
+
342
+ if (RNNFeatureToggles.isEnabled(TAB_BAR_TRANSLUCENCE) && bottomTabsOptions.translucent.isTrue) {
343
+ if (bottomTabsOptions.blurRadius.hasValue()) {
344
+ bottomTabsContainer.setBlurRadius(bottomTabsOptions.blurRadius.get().toFloat())
345
+ }
346
+ if (bottomTabsOptions.backgroundColor.hasValue()) {
347
+ bottomTabsContainer.setBlurColor(bottomTabsOptions.backgroundColor.get())
348
+ }
349
+ bottomTabsContainer.enableBackgroundBlur()
350
+ } else {
351
+ bottomTabsContainer.disableBackgroundBlur()
352
+
353
+ // TODO Change to bottomTabsContainer.setBackgroundColor()?
354
+ bottomTabs.setBackgroundColor(bottomTabsOptions.backgroundColor.get(Color.WHITE)!!)
355
+ }
250
356
 
251
357
  if (bottomTabsOptions.shadowOptions.hasValue()) {
252
358
  if (bottomTabsOptions.shadowOptions.color.hasValue())
@@ -258,4 +364,16 @@ class BottomTabsPresenter(
258
364
  bottomTabsContainer.showTopLine()
259
365
  }
260
366
  }
367
+
368
+ private fun extractCornerRadius(cornerRadius: Fraction): Float {
369
+ val radiusDp = cornerRadius.get()
370
+ val radius = UiUtils.dpToPx(bottomTabsContainer.context, radiusDp.toFloat())
371
+ return radius
372
+ }
373
+
374
+ private fun extractBottomMarginPx(bottomMargin: Fraction): Int {
375
+ val marginDp = bottomMargin.get()
376
+ val margin = UiUtils.dpToPx(bottomTabsContainer.context, marginDp.toFloat()).roundToInt()
377
+ return margin
378
+ }
261
379
  }
@@ -84,8 +84,9 @@ public class ComponentViewController extends ChildController<ComponentLayout> {
84
84
  @Override
85
85
  public void onViewWillAppear() {
86
86
  super.onViewWillAppear();
87
- if (view != null)
87
+ if (view != null) {
88
88
  view.sendComponentWillStart();
89
+ }
89
90
  }
90
91
 
91
92
  @Override
@@ -1,5 +1,8 @@
1
1
  package com.reactnativenavigation.views.bottomtabs;
2
2
 
3
+ import static com.reactnativenavigation.utils.CollectionUtils.forEach;
4
+ import static com.reactnativenavigation.utils.ViewUtils.findChildByClass;
5
+
3
6
  import android.annotation.SuppressLint;
4
7
  import android.content.Context;
5
8
  import android.graphics.Color;
@@ -7,6 +10,8 @@ import android.graphics.drawable.Drawable;
7
10
  import android.view.View;
8
11
  import android.widget.LinearLayout;
9
12
 
13
+ import androidx.annotation.IntRange;
14
+
10
15
  import com.aurelhubert.ahbottomnavigation.AHBottomNavigation;
11
16
  import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem;
12
17
  import com.reactnativenavigation.R;
@@ -15,11 +20,6 @@ import com.reactnativenavigation.options.LayoutDirection;
15
20
  import java.util.ArrayList;
16
21
  import java.util.List;
17
22
 
18
- import androidx.annotation.IntRange;
19
-
20
- import static com.reactnativenavigation.utils.CollectionUtils.*;
21
- import static com.reactnativenavigation.utils.ViewUtils.findChildByClass;
22
-
23
23
  @SuppressLint("ViewConstructor")
24
24
  public class BottomTabs extends AHBottomNavigation {
25
25
  private boolean itemsCreationEnabled = true;
@@ -60,6 +60,7 @@ public class BottomTabs extends AHBottomNavigation {
60
60
  }
61
61
  }
62
62
 
63
+ // TODO Find a better way to do this
63
64
  public void superCreateItems() {
64
65
  super.createItems();
65
66
  }
@@ -77,7 +78,6 @@ public class BottomTabs extends AHBottomNavigation {
77
78
  onItemCreationEnabled.add(() -> super.setCurrentItem(position, useCallback));
78
79
  }
79
80
  }
80
-
81
81
 
82
82
  @Override
83
83
  public void setTitleState(TitleState titleState) {
@@ -3,12 +3,20 @@ package com.reactnativenavigation.views.bottomtabs
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
5
  import android.graphics.Color
6
+ import android.graphics.Outline
7
+ import android.util.Log
6
8
  import android.view.View
9
+ import android.view.ViewOutlineProvider
10
+ import android.widget.FrameLayout.LayoutParams.MATCH_PARENT
11
+ import android.widget.FrameLayout.LayoutParams.WRAP_CONTENT
7
12
  import android.widget.LinearLayout
8
13
  import androidx.annotation.RestrictTo
9
14
  import androidx.core.graphics.ColorUtils
10
15
  import com.reactnativenavigation.options.params.Fraction
11
16
  import com.reactnativenavigation.utils.UiUtils.dpToPx
17
+ import eightbitlab.com.blurview.BlurTarget
18
+ import eightbitlab.com.blurview.BlurView
19
+ import java.lang.ref.WeakReference
12
20
  import kotlin.math.roundToInt
13
21
 
14
22
 
@@ -20,16 +28,24 @@ internal const val DEFAULT_SHADOW_ANGLE = 270f
20
28
  internal const val DEFAULT_TOP_OUTLINE_SIZE_PX = 1
21
29
  internal const val DEFAULT_TOP_OUTLINE_COLOR = Color.DKGRAY
22
30
 
31
+ private const val LOG_TAG = "BottomTabs"
32
+
23
33
  class TopOutlineView(context: Context) : View(context)
24
34
 
25
35
  @SuppressLint("ViewConstructor")
26
36
  class BottomTabsContainer(context: Context, val bottomTabs: BottomTabs) : ShadowLayout(context) {
27
37
 
38
+ private val blurringView: BlurView
39
+ private var blurringSurface = WeakReference<BlurTarget>(null)
40
+ private var blurEnabled: Boolean? = null
41
+ private var blurRadius: Float? = null
42
+ private var blurColor: Int? = null
43
+
28
44
  var topOutLineView = TopOutlineView(context)
29
45
  @RestrictTo(RestrictTo.Scope.TESTS, RestrictTo.Scope.SUBCLASSES) get
30
46
  @RestrictTo(RestrictTo.Scope.TESTS) set(value) {
31
47
  this.removeView(field)
32
- addView(value, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
48
+ addView(value, LayoutParams(MATCH_PARENT, WRAP_CONTENT))
33
49
  field = value
34
50
  }
35
51
 
@@ -39,17 +55,23 @@ class BottomTabsContainer(context: Context, val bottomTabs: BottomTabs) : Shadow
39
55
  shadowAngle = DEFAULT_SHADOW_ANGLE
40
56
  shadowDistance = DEFAULT_SHADOW_DISTANCE
41
57
  shadowColor = DEFAULT_SHADOW_COLOR
58
+
42
59
  val linearLayout = LinearLayout(context).apply {
43
60
  orientation = LinearLayout.VERTICAL
44
- this.addView(topOutLineView, LayoutParams(LayoutParams.MATCH_PARENT, DEFAULT_TOP_OUTLINE_SIZE_PX))
45
- this.addView(bottomTabs, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
61
+ addView(topOutLineView, LayoutParams(MATCH_PARENT, DEFAULT_TOP_OUTLINE_SIZE_PX))
62
+ addView(bottomTabs, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
46
63
  }
64
+
47
65
  this.clipChildren = false
48
66
  this.clipToPadding = false
49
67
  setTopOutLineColor(DEFAULT_TOP_OUTLINE_COLOR)
50
68
  this.topOutLineView.visibility = View.GONE
51
69
 
52
- this.addView(linearLayout, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
70
+ blurringView = BlurView(context).apply {
71
+ setBlurEnabled(false)
72
+ addView(linearLayout, WRAP_CONTENT, WRAP_CONTENT)
73
+ }
74
+ addView(blurringView, WRAP_CONTENT, WRAP_CONTENT)
53
75
  }
54
76
 
55
77
  override var shadowRadius: Float
@@ -70,6 +92,63 @@ class BottomTabsContainer(context: Context, val bottomTabs: BottomTabs) : Shadow
70
92
  shadowColor = ColorUtils.setAlphaComponent(shadowColor, (opacity * 0xFF).roundToInt())
71
93
  }
72
94
 
95
+ fun setBlurSurface(blurSurface: BlurTarget) {
96
+ if (blurSurface == blurringSurface.get()) {
97
+ return
98
+ }
99
+
100
+ blurringSurface = WeakReference(blurSurface)
101
+ blurringView
102
+ .setupWith(blurSurface)
103
+ .setBlurEnabled(blurEnabled == true).apply {
104
+ if (blurRadius != null) {
105
+ setBlurRadius(blurRadius!!)
106
+ }
107
+
108
+ if (blurColor != null) {
109
+ setOverlayColor(blurColor!!)
110
+ }
111
+ }
112
+ }
113
+
114
+ fun enableBackgroundBlur() {
115
+ blurringView.setBlurEnabled(true)
116
+ blurEnabled = true
117
+ }
118
+
119
+ fun disableBackgroundBlur() {
120
+ blurringView.setBlurEnabled(false)
121
+ blurEnabled = false
122
+ }
123
+
124
+ fun setBlurRadius(radius: Float) {
125
+ blurringView.setBlurRadius(radius)
126
+ blurRadius = radius
127
+ }
128
+
129
+ fun setBlurColor(color: Int) {
130
+ if (com.reactnativenavigation.utils.ColorUtils.isOpaque(color)) {
131
+ Log.w(LOG_TAG, "Opaque color (#${Integer.toHexString(color)}) set alongside blur-effect in bottom-tabs, rendering blur futile")
132
+ }
133
+
134
+ blurringView.setOverlayColor(color)
135
+ blurColor = color
136
+ }
137
+
138
+ fun setRoundedCorners(radius: Float) {
139
+ this.outlineProvider = object: ViewOutlineProvider() {
140
+ override fun getOutline(view: View, outline: Outline) {
141
+ outline.setRoundRect(0, 0, view.width, view.height, radius)
142
+ }
143
+ }
144
+ this.clipToOutline = true
145
+ }
146
+
147
+ fun clearRoundedCorners() {
148
+ this.outlineProvider = null
149
+ this.clipToOutline = true
150
+ }
151
+
73
152
  fun showShadow() {
74
153
  isShadowed = true
75
154
  }
@@ -98,4 +177,3 @@ class BottomTabsContainer(context: Context, val bottomTabs: BottomTabs) : Shadow
98
177
  setElevation(dpToPx(context, elevation.get().toFloat()))
99
178
  }
100
179
  }
101
-
@@ -1,18 +1,34 @@
1
1
  package com.reactnativenavigation.views.bottomtabs;
2
2
 
3
+ import static android.view.Gravity.CENTER_HORIZONTAL;
4
+ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
5
+ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
6
+
3
7
  import android.content.Context;
8
+ import android.graphics.Color;
4
9
  import android.view.Gravity;
5
10
  import android.view.View;
6
11
  import android.view.ViewGroup;
12
+ import android.widget.LinearLayout;
7
13
 
8
14
  import androidx.coordinatorlayout.widget.CoordinatorLayout;
9
15
 
10
- import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
11
- import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
16
+ import com.reactnativenavigation.RNNFeatureToggles;
17
+ import com.reactnativenavigation.RNNToggles;
18
+
19
+ import eightbitlab.com.blurview.BlurTarget;
12
20
 
21
+ /**
22
+ * Implementation note: The space-view and its containing linear-layout is a trick meant to force
23
+ * a bottom margin layout which isn't based on a `MarginLayoutParams.bottomMargin`. That's because
24
+ * it is not always honored by the CoordinatorLayout. This appears to be an ok work-around (which
25
+ * BTW actually would have been an idiomatic solution in ComposeUI).
26
+ */
13
27
  public class BottomTabsLayout extends CoordinatorLayout {
14
28
 
15
29
  private BottomTabsContainer bottomTabsContainer;
30
+ private View spaceView;
31
+ private BlurTarget blurSurface;
16
32
 
17
33
  public BottomTabsLayout(Context context) {
18
34
  super(context);
@@ -21,16 +37,52 @@ public class BottomTabsLayout extends CoordinatorLayout {
21
37
  @Override
22
38
  public void addView(View child, int index, ViewGroup.LayoutParams params) {
23
39
  if (bottomTabsContainer != null && child != bottomTabsContainer) {
24
- super.addView(child, getChildCount() - 1, params);
40
+ if (RNNFeatureToggles.isEnabled(RNNToggles.TAB_BAR_TRANSLUCENCE)) {
41
+ // As loosely explained in BlurView's README, the blur *view* and blur *target* must
42
+ // reside in different sub-sections of the view-hierarchy
43
+ lazyInitBlurSurface();
44
+ blurSurface.addView(child, -1, params);
45
+ } else {
46
+ super.addView(child, getChildCount() - 1, params);
47
+ }
25
48
  } else {
26
49
  super.addView(child, 0, params);
27
50
  }
51
+
52
+ if (bottomTabsContainer != null && blurSurface != null) {
53
+ bottomTabsContainer.setBlurSurface(blurSurface);
54
+ }
28
55
  }
29
56
 
30
57
  public void addBottomTabsContainer(BottomTabsContainer bottomTabsContainer) {
31
- CoordinatorLayout.LayoutParams lp = new CoordinatorLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
32
- lp.gravity = Gravity.BOTTOM;
33
- addView(bottomTabsContainer, lp);
58
+ View spaceView = new View(getContext());
59
+ spaceView.setLayoutParams(new LinearLayout.LayoutParams(WRAP_CONTENT, 0));
60
+
61
+ LinearLayout surface = new LinearLayout(getContext());
62
+ surface.setOrientation(LinearLayout.VERTICAL);
63
+ surface.setGravity(CENTER_HORIZONTAL);
64
+ surface.setBackgroundColor(Color.TRANSPARENT);
65
+ surface.addView(bottomTabsContainer, new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
66
+ surface.addView(spaceView);
67
+
68
+ // Note: Width should always be WRAP_CONTENT so we could delegate the width-related decision
69
+ // making to the bottom-tabs view hierarchy itself (i.e. based on user properties).
70
+ CoordinatorLayout.LayoutParams lp = new CoordinatorLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
71
+ lp.gravity = Gravity.BOTTOM | CENTER_HORIZONTAL;
72
+ addView(surface, -1, lp);
73
+
34
74
  this.bottomTabsContainer = bottomTabsContainer;
75
+ this.spaceView = spaceView;
76
+ }
77
+
78
+ public void setBottomMargin(int bottomMargin) {
79
+ ((MarginLayoutParams) spaceView.getLayoutParams()).bottomMargin = bottomMargin;
80
+ }
81
+
82
+ private void lazyInitBlurSurface() {
83
+ if (blurSurface == null) {
84
+ blurSurface = new BlurTarget(getContext());
85
+ super.addView(blurSurface, super.getChildCount() - 1, new CoordinatorLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
86
+ }
35
87
  }
36
88
  }
@@ -13,7 +13,7 @@ private const val MAX_ANGLE = 360.0f
13
13
  private const val MIN_RADIUS = 0.1f
14
14
  private const val MIN_ANGLE = 0.0f
15
15
 
16
- open class ShadowLayout constructor(context: Context) : FrameLayout(context) {
16
+ open class ShadowLayout(context: Context) : FrameLayout(context) {
17
17
  private val paint: Paint = Paint(ANTI_ALIAS_FLAG).apply {
18
18
  isDither = true
19
19
  isFilterBitmap = true
@@ -43,7 +43,7 @@ open class ShadowLayout constructor(context: Context) : FrameLayout(context) {
43
43
  var isShadowed: Boolean = false
44
44
  set(isShadowed) {
45
45
  field = isShadowed
46
- this.updatePadding()
46
+ updatePadding()
47
47
  postInvalidate()
48
48
  }
49
49
 
@@ -108,11 +108,8 @@ open class ShadowLayout constructor(context: Context) : FrameLayout(context) {
108
108
  if (isShadowed) {
109
109
  if (invalidateShadow) {
110
110
  if (bounds.width() != 0 && bounds.height() != 0) {
111
- bitmap = Bitmap.createBitmap(
112
- bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888
113
- )
114
- bitmap?.let {
115
- mainCanvas.setBitmap(bitmap)
111
+ bitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888).also {
112
+ mainCanvas.setBitmap(it)
116
113
  invalidateShadow = false
117
114
 
118
115
  super.dispatchDraw(mainCanvas)
@@ -135,5 +132,4 @@ open class ShadowLayout constructor(context: Context) : FrameLayout(context) {
135
132
 
136
133
  super.dispatchDraw(canvas)
137
134
  }
138
-
139
135
  }
@@ -773,7 +773,9 @@ export interface OptionsBottomTabs {
773
773
  */
774
774
  drawBehind?: boolean;
775
775
  /**
776
- * Set a background color for the bottom tabs
776
+ * Set a background color for the bottom tabs.<br/>
777
+ * On Android - also applicable when translucence is applied, but a semi-transparent
778
+ * color should be used (e.g. `rgba(255, 0, 0, 0.25)`).
777
779
  */
778
780
  backgroundColor?: Color;
779
781
  /**
@@ -789,10 +791,42 @@ export interface OptionsBottomTabs {
789
791
  */
790
792
  barStyle?: 'default' | 'black';
791
793
  /**
792
- * Allows the Bottom Tabs to be translucent (blurred)
793
- * #### (iOS specific)
794
+ * Control the way the bottom tabs are laid out.
795
+ * - `stretch`: Fill the entire width of the screen.
796
+ * - `compact`: Occupy the minimum width needed to hold tab buttons. Recommended for
797
+ * usage in conjunction with `drawBehind: true`.
798
+ *
799
+ * #### (Android specific)
800
+ * @default 'stretch'
801
+ */
802
+ layoutStyle?: 'stretch' | 'compact';
803
+ /**
804
+ * Specify a corner-radius (in dip) in order to apply round corners to the tabs container.<br/>
805
+ * Mainly suitable when used in conjunction with `layoutStyle: 'compact'`
806
+ * #### (Android specific)
807
+ */
808
+ cornerRadius?: AndroidDensityNumber;
809
+ /**
810
+ * Bottom-margin to set in order to apply a "hover" effect.
811
+ * Works best when used in conjunction with `layoutStyle: 'compact'` and `drawBehind: true`.
812
+ * #### (Android specific)
813
+ */
814
+ bottomMargin?: AndroidDensityNumber;
815
+ /**
816
+ * Allows the bottom tabs to be translucent (blurred). Doesn't necessarily play
817
+ * nice with shadow effects on Android.
818
+ * #### Android: experimental, turn on using native toggle `TAB_BAR_TRANSLUCENCE`.
794
819
  */
795
820
  translucent?: boolean;
821
+ /**
822
+ * Set a custom radius to be used in the blur effect. Higher is blurrier, but
823
+ * also more CPU-intensive.<br/>
824
+ * Note: The blurring is performed following a bitmap downscale of x4.0, so
825
+ * ultimately the actual radius is (4*blurRadius).
826
+ * #### (Android specific)
827
+ * @defaultValue 1.0
828
+ */
829
+ blurRadius?: AndroidDensityNumber;
796
830
  /**
797
831
  * Hide the top line of the Tab Bar
798
832
  * #### (iOS specific)
@@ -855,7 +855,9 @@ export interface OptionsBottomTabs {
855
855
  */
856
856
  drawBehind?: boolean;
857
857
  /**
858
- * Set a background color for the bottom tabs
858
+ * Set a background color for the bottom tabs.<br/>
859
+ * On Android - also applicable when translucence is applied, but a semi-transparent
860
+ * color should be used (e.g. `rgba(255, 0, 0, 0.25)`).
859
861
  */
860
862
  backgroundColor?: Color;
861
863
  /**
@@ -871,10 +873,42 @@ export interface OptionsBottomTabs {
871
873
  */
872
874
  barStyle?: 'default' | 'black';
873
875
  /**
874
- * Allows the Bottom Tabs to be translucent (blurred)
875
- * #### (iOS specific)
876
+ * Control the way the bottom tabs are laid out.
877
+ * - `stretch`: Fill the entire width of the screen.
878
+ * - `compact`: Occupy the minimum width needed to hold tab buttons. Recommended for
879
+ * usage in conjunction with `drawBehind: true`.
880
+ *
881
+ * #### (Android specific)
882
+ * @default 'stretch'
883
+ */
884
+ layoutStyle?: 'stretch' | 'compact';
885
+ /**
886
+ * Specify a corner-radius (in dip) in order to apply round corners to the tabs container.<br/>
887
+ * Mainly suitable when used in conjunction with `layoutStyle: 'compact'`
888
+ * #### (Android specific)
889
+ */
890
+ cornerRadius?: AndroidDensityNumber;
891
+ /**
892
+ * Bottom-margin to set in order to apply a "hover" effect.
893
+ * Works best when used in conjunction with `layoutStyle: 'compact'` and `drawBehind: true`.
894
+ * #### (Android specific)
895
+ */
896
+ bottomMargin?: AndroidDensityNumber;
897
+ /**
898
+ * Allows the bottom tabs to be translucent (blurred). Doesn't necessarily play
899
+ * nice with shadow effects on Android.
900
+ * #### Android: experimental, turn on using native toggle `TAB_BAR_TRANSLUCENCE`.
876
901
  */
877
902
  translucent?: boolean;
903
+ /**
904
+ * Set a custom radius to be used in the blur effect. Higher is blurrier, but
905
+ * also more CPU-intensive.<br/>
906
+ * Note: The blurring is performed following a bitmap downscale of x4.0, so
907
+ * ultimately the actual radius is (4*blurRadius).
908
+ * #### (Android specific)
909
+ * @defaultValue 1.0
910
+ */
911
+ blurRadius?: AndroidDensityNumber;
878
912
  /**
879
913
  * Hide the top line of the Tab Bar
880
914
  * #### (iOS specific)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-navigation",
3
- "version": "8.1.2",
3
+ "version": "8.2.0",
4
4
  "description": "React Native Navigation - truly native navigation for iOS and Android",
5
5
  "license": "MIT",
6
6
  "nativePackage": true,