-
Notifications
You must be signed in to change notification settings - Fork 220
/
FragNavController.java
1040 lines (884 loc) · 37 KB
/
FragNavController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package com.ncapdevi.fragnav;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.IdRes;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.util.Pair;
import android.view.View;
import org.json.JSONArray;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* The class is used to manage navigation through multiple stacks of fragments, as well as coordinate
* fragments that may appear on screen
* <p>
* https://github.com/ncapdevi/FragNav
* Nic Capdevila
* Nic.Capdevila@gmail.com
* <p>
* Originally Created March 2016
*/
@SuppressWarnings("RestrictedApi")
public class FragNavController {
// Declare the constants. A maximum of 5 tabs is recommended for bottom navigation, this is per Material Design's Bottom Navigation's design spec.
public static final int NO_TAB = -1;
public static final int TAB1 = 0;
public static final int TAB2 = 1;
public static final int TAB3 = 2;
public static final int TAB4 = 3;
public static final int TAB5 = 4;
public static final int TAB6 = 5;
public static final int TAB7 = 6;
public static final int TAB8 = 7;
public static final int TAB9 = 8;
public static final int TAB10 = 9;
public static final int TAB11 = 10;
public static final int TAB12 = 11;
public static final int TAB13 = 12;
public static final int TAB14 = 13;
public static final int TAB15 = 14;
public static final int TAB16 = 15;
public static final int TAB17 = 16;
public static final int TAB18 = 17;
public static final int TAB19 = 18;
public static final int TAB20 = 19;
private static final int MAX_NUM_TABS = 20;
// Extras used to store savedInstanceState
private static final String EXTRA_TAG_COUNT = FragNavController.class.getName() + ":EXTRA_TAG_COUNT";
private static final String EXTRA_SELECTED_TAB_INDEX = FragNavController.class.getName() + ":EXTRA_SELECTED_TAB_INDEX";
private static final String EXTRA_CURRENT_FRAGMENT = FragNavController.class.getName() + ":EXTRA_CURRENT_FRAGMENT";
private static final String EXTRA_FRAGMENT_STACK = FragNavController.class.getName() + ":EXTRA_FRAGMENT_STACK";
@IdRes
private final int mContainerId;
@NonNull
private final List<Stack<Fragment>> mFragmentStacks;
@NonNull
private final FragmentManager mFragmentManager;
private final FragNavTransactionOptions mDefaultTransactionOptions;
@TabIndex
private int mSelectedTabIndex;
private int mTagCount;
@Nullable
private Fragment mCurrentFrag;
@Nullable
private DialogFragment mCurrentDialogFrag;
@Nullable
private RootFragmentListener mRootFragmentListener;
@Nullable
private TransactionListener mTransactionListener;
private boolean mExecutingTransaction;
//region Construction and setup
private FragNavController(Builder builder, @Nullable Bundle savedInstanceState) {
mFragmentManager = builder.mFragmentManager;
mContainerId = builder.mContainerId;
mFragmentStacks = new ArrayList<>(builder.mNumberOfTabs);
mRootFragmentListener = builder.mRootFragmentListener;
mTransactionListener = builder.mTransactionListener;
mDefaultTransactionOptions = builder.mDefaultTransactionOptions;
mSelectedTabIndex = builder.mSelectedTabIndex;
//Attempt to restore from bundle, if not, initialize
if (!restoreFromBundle(savedInstanceState, builder.mRootFragments)) {
for (int i = 0; i < builder.mNumberOfTabs; i++) {
Stack<Fragment> stack = new Stack<>();
if (builder.mRootFragments != null) {
stack.add(builder.mRootFragments.get(i));
}
mFragmentStacks.add(stack);
}
initialize(builder.mSelectedTabIndex);
}
}
public static Builder newBuilder(@Nullable Bundle savedInstanceState, FragmentManager fragmentManager, int containerId) {
return new Builder(savedInstanceState, fragmentManager, containerId);
}
//endregion
//region Transactions
/**
* Function used to switch to the specified fragment stack
*
* @param index The given index to switch to
* @param transactionOptions Transaction options to be displayed
* @throws IndexOutOfBoundsException Thrown if trying to switch to an index outside given range
*/
public void switchTab(@TabIndex int index, @Nullable FragNavTransactionOptions transactionOptions) throws IndexOutOfBoundsException {
//Check to make sure the tab is within range
if (index >= mFragmentStacks.size()) {
throw new IndexOutOfBoundsException("Can't switch to a tab that hasn't been initialized, " +
"Index : " + index + ", current stack size : " + mFragmentStacks.size() +
". Make sure to create all of the tabs you need in the Constructor or provide a way for them to be created via RootFragmentListener.");
}
if (mSelectedTabIndex != index) {
mSelectedTabIndex = index;
FragmentTransaction ft = createTransactionWithOptions(transactionOptions, false);
detachCurrentFragment(ft);
Fragment fragment = null;
if (index == NO_TAB) {
commitTransaction(ft, transactionOptions);
} else {
//Attempt to reattach previous fragment
fragment = reattachPreviousFragment(ft);
if (fragment != null) {
commitTransaction(ft, transactionOptions);
} else {
fragment = getRootFragment(mSelectedTabIndex);
ft.add(mContainerId, fragment, generateTag(fragment));
commitTransaction(ft, transactionOptions);
}
}
mCurrentFrag = fragment;
if (mTransactionListener != null) {
mTransactionListener.onTabTransaction(mCurrentFrag, mSelectedTabIndex);
}
}
}
/**
* Function used to switch to the specified fragment stack
*
* @param index The given index to switch to
* @throws IndexOutOfBoundsException Thrown if trying to switch to an index outside given range
*/
public void switchTab(@TabIndex int index) throws IndexOutOfBoundsException {
switchTab(index, null);
}
/**
* Push a fragment onto the current stack
*
* @param fragment The fragment that is to be pushed
* @param transactionOptions Transaction options to be displayed
*/
public void pushFragment(@Nullable Fragment fragment, @Nullable FragNavTransactionOptions transactionOptions) {
if (fragment != null && mSelectedTabIndex != NO_TAB) {
FragmentTransaction ft = createTransactionWithOptions(transactionOptions, false);
detachCurrentFragment(ft);
ft.add(mContainerId, fragment, generateTag(fragment));
commitTransaction(ft, transactionOptions);
mFragmentStacks.get(mSelectedTabIndex).push(fragment);
mCurrentFrag = fragment;
if (mTransactionListener != null) {
mTransactionListener.onFragmentTransaction(mCurrentFrag, TransactionType.PUSH);
}
}
}
/**
* Push a fragment onto the current stack
*
* @param fragment The fragment that is to be pushed
*/
public void pushFragment(@Nullable Fragment fragment) {
pushFragment(fragment, null);
}
/**
* Pop the current fragment from the current tab
*
* @param transactionOptions Transaction options to be displayed
*/
public void popFragment(@Nullable FragNavTransactionOptions transactionOptions) throws UnsupportedOperationException {
popFragments(1, transactionOptions);
}
/**
* Pop the current fragment from the current tab
*/
public void popFragment() throws UnsupportedOperationException {
popFragment(null);
}
/**
* Pop the current stack until a given tag is found. If the tag is not found, the stack will popFragment until it is at
* the root fragment
*
* @param transactionOptions Transaction options to be displayed
*/
public void popFragments(int popDepth, @Nullable FragNavTransactionOptions transactionOptions) throws UnsupportedOperationException {
if (isRootFragment()) {
throw new UnsupportedOperationException(
"You can not popFragment the rootFragment. If you need to change this fragment, use replaceFragment(fragment)");
} else if (popDepth < 1) {
throw new UnsupportedOperationException("popFragments parameter needs to be greater than 0");
} else if (mSelectedTabIndex == NO_TAB) {
throw new UnsupportedOperationException("You can not pop fragments when no tab is selected");
}
//If our popDepth is big enough that it would just clear the stack, then call that.
if (popDepth >= mFragmentStacks.get(mSelectedTabIndex).size() - 1) {
clearStack(transactionOptions);
return;
}
Fragment fragment;
FragmentTransaction ft = createTransactionWithOptions(transactionOptions, true);
//Pop the number of the fragments on the stack and remove them from the FragmentManager
for (int i = 0; i < popDepth; i++) {
fragment = mFragmentManager.findFragmentByTag(mFragmentStacks.get(mSelectedTabIndex).pop().getTag());
if (fragment != null) {
ft.remove(fragment);
}
}
//Attempt to reattach previous fragment
fragment = reattachPreviousFragment(ft);
boolean bShouldPush = false;
//If we can't reattach, either pull from the stack, or create a new root fragment
if (fragment != null) {
commitTransaction(ft, transactionOptions);
} else {
if (!mFragmentStacks.get(mSelectedTabIndex).isEmpty()) {
fragment = mFragmentStacks.get(mSelectedTabIndex).peek();
ft.add(mContainerId, fragment, fragment.getTag());
commitTransaction(ft, transactionOptions);
} else {
fragment = getRootFragment(mSelectedTabIndex);
ft.add(mContainerId, fragment, generateTag(fragment));
commitTransaction(ft, transactionOptions);
bShouldPush = true;
}
}
//Need to have this down here so that that tag has been
// committed to the fragment before we add to the stack
if (bShouldPush) {
mFragmentStacks.get(mSelectedTabIndex).push(fragment);
}
mCurrentFrag = fragment;
if (mTransactionListener != null) {
mTransactionListener.onFragmentTransaction(mCurrentFrag, TransactionType.POP);
}
}
/**
* Pop the current fragment from the current tab
*/
public void popFragments(int popDepth) throws UnsupportedOperationException {
popFragments(popDepth, null);
}
/**
* Clears the current tab's stack to get to just the bottom Fragment. This will reveal the root fragment
*
* @param transactionOptions Transaction options to be displayed
*/
public void clearStack(@Nullable FragNavTransactionOptions transactionOptions) {
if (mSelectedTabIndex == NO_TAB) {
return;
}
//Grab Current stack
Stack<Fragment> fragmentStack = mFragmentStacks.get(mSelectedTabIndex);
// Only need to start popping and reattach if the stack is greater than 1
if (fragmentStack.size() > 1) {
Fragment fragment;
FragmentTransaction ft = createTransactionWithOptions(transactionOptions, true);
//Pop all of the fragments on the stack and remove them from the FragmentManager
while (fragmentStack.size() > 1) {
fragment = mFragmentManager.findFragmentByTag(fragmentStack.pop().getTag());
if (fragment != null) {
ft.remove(fragment);
}
}
//Attempt to reattach previous fragment
fragment = reattachPreviousFragment(ft);
boolean bShouldPush = false;
//If we can't reattach, either pull from the stack, or create a new root fragment
if (fragment != null) {
commitTransaction(ft, transactionOptions);
} else {
if (!fragmentStack.isEmpty()) {
fragment = fragmentStack.peek();
ft.add(mContainerId, fragment, fragment.getTag());
commitTransaction(ft, transactionOptions);
} else {
fragment = getRootFragment(mSelectedTabIndex);
ft.add(mContainerId, fragment, generateTag(fragment));
commitTransaction(ft, transactionOptions);
bShouldPush = true;
}
}
if (bShouldPush) {
mFragmentStacks.get(mSelectedTabIndex).push(fragment);
}
//Update the stored version we have in the list
mFragmentStacks.set(mSelectedTabIndex, fragmentStack);
mCurrentFrag = fragment;
if (mTransactionListener != null) {
mTransactionListener.onFragmentTransaction(mCurrentFrag, TransactionType.POP);
}
}
}
/**
* Clears the current tab's stack to get to just the bottom Fragment. This will reveal the root fragment.
*/
public void clearStack() {
clearStack(null);
}
/**
* Replace the current fragment
*
* @param fragment the fragment to be shown instead
* @param transactionOptions Transaction options to be displayed
*/
public void replaceFragment(@NonNull Fragment fragment, @Nullable FragNavTransactionOptions transactionOptions) {
Fragment poppingFrag = getCurrentFrag();
if (poppingFrag != null) {
FragmentTransaction ft = createTransactionWithOptions(transactionOptions, false);
//overly cautious fragment popFragment
Stack<Fragment> fragmentStack = mFragmentStacks.get(mSelectedTabIndex);
if (!fragmentStack.isEmpty()) {
fragmentStack.pop();
}
String tag = generateTag(fragment);
ft.replace(mContainerId, fragment, tag);
//Commit our transactions
commitTransaction(ft, transactionOptions);
fragmentStack.push(fragment);
mCurrentFrag = fragment;
if (mTransactionListener != null) {
mTransactionListener.onFragmentTransaction(mCurrentFrag, TransactionType.REPLACE);
}
}
}
/**
* Replace the current fragment
*
* @param fragment the fragment to be shown instead
*/
public void replaceFragment(@NonNull Fragment fragment) {
replaceFragment(fragment, null);
}
/**
* @return Current DialogFragment being displayed. Null if none
*/
@Nullable
@CheckResult
public DialogFragment getCurrentDialogFrag() {
if (mCurrentDialogFrag != null) {
return mCurrentDialogFrag;
}
//Else try to find one in the FragmentManager
else {
FragmentManager fragmentManager;
if (mCurrentFrag != null) {
fragmentManager = mCurrentFrag.getChildFragmentManager();
} else {
fragmentManager = mFragmentManager;
}
if (fragmentManager.getFragments() != null) {
for (Fragment fragment : fragmentManager.getFragments()) {
if (fragment instanceof DialogFragment) {
mCurrentDialogFrag = (DialogFragment) fragment;
break;
}
}
}
}
return mCurrentDialogFrag;
}
/**
* Clear any DialogFragments that may be shown
*/
public void clearDialogFragment() {
if (mCurrentDialogFrag != null) {
mCurrentDialogFrag.dismiss();
mCurrentDialogFrag = null;
}
// If we don't have the current dialog, try to find and dismiss it
else {
FragmentManager fragmentManager;
if (mCurrentFrag != null) {
fragmentManager = mCurrentFrag.getChildFragmentManager();
} else {
fragmentManager = mFragmentManager;
}
if (fragmentManager.getFragments() != null) {
for (Fragment fragment : fragmentManager.getFragments()) {
if (fragment instanceof DialogFragment) {
((DialogFragment) fragment).dismiss();
}
}
}
}
}
/**
* Display a DialogFragment on the screen
*
* @param dialogFragment The Fragment to be Displayed
*/
public void showDialogFragment(@Nullable DialogFragment dialogFragment) {
if (dialogFragment != null) {
FragmentManager fragmentManager;
if (mCurrentFrag != null) {
fragmentManager = mCurrentFrag.getChildFragmentManager();
} else {
fragmentManager = mFragmentManager;
}
//Clear any current dialog fragments
if (fragmentManager.getFragments() != null) {
for (Fragment fragment : fragmentManager.getFragments()) {
if (fragment instanceof DialogFragment) {
((DialogFragment) fragment).dismiss();
mCurrentDialogFrag = null;
}
}
}
mCurrentDialogFrag = dialogFragment;
try {
dialogFragment.show(fragmentManager, dialogFragment.getClass().getName());
} catch (IllegalStateException e) {
// Activity was likely destroyed before we had a chance to show, nothing can be done here.
}
}
}
//endregion
//region Private helper functions
/**
* Helper function to make sure that we are starting with a clean slate and to perform our first fragment interaction.
*
* @param index the tab index to initialize to
*/
private void initialize(@TabIndex int index) {
mSelectedTabIndex = index;
if (mSelectedTabIndex > mFragmentStacks.size()) {
throw new IndexOutOfBoundsException("Starting index cannot be larger than the number of stacks");
}
mSelectedTabIndex = index;
clearFragmentManager();
clearDialogFragment();
if (index == NO_TAB) {
return;
}
FragmentTransaction ft = createTransactionWithOptions(null, false);
Fragment fragment = getRootFragment(index);
ft.add(mContainerId, fragment, generateTag(fragment));
commitTransaction(ft, null);
mCurrentFrag = fragment;
if (mTransactionListener != null) {
mTransactionListener.onTabTransaction(mCurrentFrag, mSelectedTabIndex);
}
}
/**
* Helper function to get the root fragment for a given index. This is done by either passing them in the constructor, or dynamically via NavListener.
*
* @param index The tab index to get this fragment from
* @return The root fragment at this index
* @throws IllegalStateException This will be thrown if we can't find a rootFragment for this index. Either because you didn't provide it in the
* constructor, or because your RootFragmentListener.getRootFragment(index) isn't returning a fragment for this index.
*/
@NonNull
@CheckResult
private Fragment getRootFragment(int index) throws IllegalStateException {
Fragment fragment = null;
if (!mFragmentStacks.get(index).isEmpty()) {
fragment = mFragmentStacks.get(index).peek();
} else if (mRootFragmentListener != null) {
fragment = mRootFragmentListener.getRootFragment(index);
if (mSelectedTabIndex != NO_TAB) {
mFragmentStacks.get(mSelectedTabIndex).push(fragment);
}
}
if (fragment == null) {
throw new IllegalStateException("Either you haven't past in a fragment at this index in your constructor, or you haven't " +
"provided a way to create it while via your RootFragmentListener.getRootFragment(index)");
}
return fragment;
}
/**
* Will attempt to reattach a previous fragment in the FragmentManager, or return null if not able to.
*
* @param ft current fragment transaction
* @return Fragment if we were able to find and reattach it
*/
@Nullable
private Fragment reattachPreviousFragment(@NonNull FragmentTransaction ft) {
Stack<Fragment> fragmentStack = mFragmentStacks.get(mSelectedTabIndex);
Fragment fragment = null;
if (!fragmentStack.isEmpty()) {
fragment = mFragmentManager.findFragmentByTag(fragmentStack.peek().getTag());
if (fragment != null) {
ft.attach(fragment);
}
}
return fragment;
}
/**
* Attempts to detach any current fragment if it exists, and if none is found, returns.
*
* @param ft the current transaction being performed
*/
private void detachCurrentFragment(@NonNull FragmentTransaction ft) {
Fragment oldFrag = getCurrentFrag();
if (oldFrag != null) {
ft.detach(oldFrag);
}
}
/**
* Helper function to attempt to get current fragment
*
* @return Fragment the current frag to be returned
*/
@Nullable
@CheckResult
public Fragment getCurrentFrag() {
//Attempt to used stored current fragment
if (mCurrentFrag != null) {
return mCurrentFrag;
} else if (mSelectedTabIndex == NO_TAB) {
return null;
}
//if not, try to pull it from the stack
else {
Stack<Fragment> fragmentStack = mFragmentStacks.get(mSelectedTabIndex);
if (!fragmentStack.isEmpty()) {
mCurrentFrag = mFragmentManager.findFragmentByTag(mFragmentStacks.get(mSelectedTabIndex).peek().getTag());
}
}
return mCurrentFrag;
}
/**
* Create a unique fragment tag so that we can grab the fragment later from the FragmentManger
*
* @param fragment The fragment that we're creating a unique tag for
* @return a unique tag using the fragment's class name
*/
@NonNull
@CheckResult
private String generateTag(@NonNull Fragment fragment) {
return fragment.getClass().getName() + ++mTagCount;
}
/**
* This check is here to prevent recursive entries into executePendingTransactions
*/
private void executePendingTransactions() {
if (!mExecutingTransaction) {
mExecutingTransaction = true;
mFragmentManager.executePendingTransactions();
mExecutingTransaction = false;
}
}
/**
* Private helper function to clear out the fragment manager on initialization. All fragment management should be done via FragNav.
*/
private void clearFragmentManager() {
if (mFragmentManager.getFragments() != null) {
FragmentTransaction ft = createTransactionWithOptions(null, false);
for (Fragment fragment : mFragmentManager.getFragments()) {
if (fragment != null) {
ft.remove(fragment);
}
}
commitTransaction(ft, null);
}
}
/**
* Setup a fragment transaction with the given option
*
* @param transactionOptions The options that will be set for this transaction
* @param isPopping
*/
@CheckResult
private FragmentTransaction createTransactionWithOptions(@Nullable FragNavTransactionOptions transactionOptions, boolean isPopping) {
FragmentTransaction ft = mFragmentManager.beginTransaction();
if (transactionOptions == null) {
transactionOptions = mDefaultTransactionOptions;
}
if (transactionOptions != null) {
if (isPopping) {
ft.setCustomAnimations(transactionOptions.popEnterAnimation, transactionOptions.popExitAnimation);
} else {
ft.setCustomAnimations(transactionOptions.enterAnimation, transactionOptions.exitAnimation);
}
ft.setTransitionStyle(transactionOptions.transitionStyle);
ft.setTransition(transactionOptions.transition);
if (transactionOptions.sharedElements != null) {
for (Pair<View, String> sharedElement : transactionOptions.sharedElements) {
ft.addSharedElement(sharedElement.first, sharedElement.second);
}
}
if (transactionOptions.breadCrumbTitle != null) {
ft.setBreadCrumbTitle(transactionOptions.breadCrumbTitle);
}
if (transactionOptions.breadCrumbShortTitle != null) {
ft.setBreadCrumbShortTitle(transactionOptions.breadCrumbShortTitle);
}
}
return ft;
}
/**
* Helper function to commit fragment transaction with transaction option - allowStateLoss
*k
* @param fragmentTransaction
* @param transactionOptions
*/
private void commitTransaction(FragmentTransaction fragmentTransaction, @Nullable FragNavTransactionOptions transactionOptions) {
if (transactionOptions != null && transactionOptions.allowStateLoss) {
fragmentTransaction.commitAllowingStateLoss();
} else {
fragmentTransaction.commit();
}
executePendingTransactions();
}
//endregion
//region Public helper functions
/**
* Get the number of fragment stacks
*
* @return the number of fragment stacks
*/
@CheckResult
public int getSize() {
return mFragmentStacks.size();
}
/**
* Get a copy of the stack at a given index
*
* @return requested stack
*/
@SuppressWarnings("unchecked")
@CheckResult
@Nullable
public Stack<Fragment> getStack(@TabIndex int index) {
if (index == NO_TAB) return null;
if (index >= mFragmentStacks.size()) {
throw new IndexOutOfBoundsException("Can't get an index that's larger than we've setup");
}
return (Stack<Fragment>) mFragmentStacks.get(index).clone();
}
/**
* Get a copy of the current stack that is being displayed
*
* @return Current stack
*/
@SuppressWarnings("unchecked")
@CheckResult
@Nullable
public Stack<Fragment> getCurrentStack() {
return getStack(mSelectedTabIndex);
}
/**
* Get the index of the current stack that is being displayed
*
* @return Current stack index
*/
@CheckResult
@TabIndex
public int getCurrentStackIndex() {
return mSelectedTabIndex;
}
/**
* @return If true, you are at the bottom of the stack
* (Consider using replaceFragment if you need to change the root fragment for some reason)
* else you can popFragment as needed as your are not at the root
*/
@CheckResult
public boolean isRootFragment() {
Stack<Fragment> stack = getCurrentStack();
return stack == null || stack.size() == 1;
}
/**
* Helper function to get wether the fragmentManger has gone through a stateSave, if this is true, you probably want to commit allowing stateloss
*
* @return if fragmentManger isStateSaved
*/
public boolean isStateSaved(){
return mFragmentManager.isStateSaved();
}
//endregion
//region SavedInstanceState
/**
* Call this in your Activity's onSaveInstanceState(Bundle outState) method to save the instance's state.
*
* @param outState The Bundle to save state information to
*/
public void onSaveInstanceState(@NonNull Bundle outState) {
// Write tag count
outState.putInt(EXTRA_TAG_COUNT, mTagCount);
// Write select tab
outState.putInt(EXTRA_SELECTED_TAB_INDEX, mSelectedTabIndex);
// Write current fragment
if (mCurrentFrag != null) {
outState.putString(EXTRA_CURRENT_FRAGMENT, mCurrentFrag.getTag());
}
// Write stacks
try {
final JSONArray stackArrays = new JSONArray();
for (Stack<Fragment> stack : mFragmentStacks) {
final JSONArray stackArray = new JSONArray();
for (Fragment fragment : stack) {
stackArray.put(fragment.getTag());
}
stackArrays.put(stackArray);
}
outState.putString(EXTRA_FRAGMENT_STACK, stackArrays.toString());
} catch (Throwable t) {
// Nothing we can do
}
}
/**
* Restores this instance to the state specified by the contents of savedInstanceState
*
* @param savedInstanceState The bundle to restore from
* @param rootFragments List of root fragments from which to initialize empty stacks. If null, pull fragments from RootFragmentListener.
* @return true if successful, false if not
*/
private boolean restoreFromBundle(@Nullable Bundle savedInstanceState, @Nullable List<Fragment> rootFragments) {
if (savedInstanceState == null) {
return false;
}
// Restore tag count
mTagCount = savedInstanceState.getInt(EXTRA_TAG_COUNT, 0);
// Restore current fragment
mCurrentFrag = mFragmentManager.findFragmentByTag(savedInstanceState.getString(EXTRA_CURRENT_FRAGMENT));
// Restore fragment stacks
try {
final JSONArray stackArrays = new JSONArray(savedInstanceState.getString(EXTRA_FRAGMENT_STACK));
for (int x = 0; x < stackArrays.length(); x++) {
final JSONArray stackArray = stackArrays.getJSONArray(x);
final Stack<Fragment> stack = new Stack<>();
if (stackArray.length() == 1) {
final String tag = stackArray.getString(0);
final Fragment fragment;
if (tag == null || "null".equalsIgnoreCase(tag)) {
if (rootFragments != null) {
fragment = rootFragments.get(x);
} else {
fragment = getRootFragment(x);
}
} else {
fragment = mFragmentManager.findFragmentByTag(tag);
}
if (fragment != null) {
stack.add(fragment);
}
} else {
for (int y = 0; y < stackArray.length(); y++) {
final String tag = stackArray.getString(y);
if (tag != null && !"null".equalsIgnoreCase(tag)) {
final Fragment fragment = mFragmentManager.findFragmentByTag(tag);
if (fragment != null) {
stack.add(fragment);
}
}
}
}
mFragmentStacks.add(stack);
}
// Restore selected tab if we have one
int selectedTabIndex = savedInstanceState.getInt(EXTRA_SELECTED_TAB_INDEX);
if (selectedTabIndex >= 0 && selectedTabIndex < MAX_NUM_TABS) {
switchTab(selectedTabIndex, null);
}
//Successfully restored state
return true;
} catch (Throwable t) {
return false;
}
}
//endregion
public enum TransactionType {
PUSH,
POP,
REPLACE
}
//Declare the TabIndex annotation
@IntDef({NO_TAB, TAB1, TAB2, TAB3, TAB4, TAB5, TAB6, TAB7, TAB8, TAB9, TAB10, TAB11, TAB12,
TAB13, TAB14, TAB15, TAB16, TAB17, TAB18, TAB19, TAB20})
@Retention(RetentionPolicy.SOURCE)
public @interface TabIndex {
}
// Declare Transit Styles
@IntDef({FragmentTransaction.TRANSIT_NONE, FragmentTransaction.TRANSIT_FRAGMENT_OPEN, FragmentTransaction.TRANSIT_FRAGMENT_CLOSE, FragmentTransaction.TRANSIT_FRAGMENT_FADE})
@Retention(RetentionPolicy.SOURCE)
@interface Transit {
}
public interface RootFragmentListener {
/**
* Dynamically create the Fragment that will go on the bottom of the stack
*
* @param index the index that the root of the stack Fragment needs to go
* @return the new Fragment
*/
Fragment getRootFragment(int index);
}
public interface TransactionListener {
void onTabTransaction(Fragment fragment, int index);
void onFragmentTransaction(Fragment fragment, TransactionType transactionType);
}
public static final class Builder {
private final int mContainerId;
private FragmentManager mFragmentManager;
private RootFragmentListener mRootFragmentListener;
@TabIndex
private int mSelectedTabIndex = TAB1;
private TransactionListener mTransactionListener;
private FragNavTransactionOptions mDefaultTransactionOptions;
private int mNumberOfTabs = 0;
private List<Fragment> mRootFragments;
private Bundle mSavedInstanceState;
public Builder(@Nullable Bundle savedInstanceState, FragmentManager mFragmentManager, int mContainerId) {
this.mSavedInstanceState = savedInstanceState;
this.mFragmentManager = mFragmentManager;
this.mContainerId = mContainerId;
}
/**
* @param selectedTabIndex The initial tab index to be used must be in range of rootFragments size
*/
public Builder selectedTabIndex(@TabIndex int selectedTabIndex) {
mSelectedTabIndex = selectedTabIndex;
if (mRootFragments != null && mSelectedTabIndex > mNumberOfTabs) {
throw new IndexOutOfBoundsException("Starting index cannot be larger than the number of stacks");
}
return this;
}
/**
* @param rootFragment A single root fragment. This library can still be helpful when managing a single stack of fragments
*/
public Builder rootFragment(Fragment rootFragment) {
mRootFragments = new ArrayList<>(1);
mRootFragments.add(rootFragment);
mNumberOfTabs = 1;
return rootFragments(mRootFragments);
}
/**
* @param rootFragments a list of root fragments. root Fragments are the root fragments that exist on any tab structure. If only one fragment is sent in, fragnav will still manage
* transactions
*/
public Builder rootFragments(@NonNull List<Fragment> rootFragments) {
mRootFragments = rootFragments;
mNumberOfTabs = rootFragments.size();
if (mNumberOfTabs > MAX_NUM_TABS) {
throw new IllegalArgumentException("Number of root fragments cannot be greater than " + MAX_NUM_TABS);
}
return this;
}
/**