Skip to content

Commit 235e902

Browse files
Liraz ShalomLiraz Shalom
authored andcommitted
UI Polish: Toolbar, Multi-Select, Slider, Empty State, and RTL
Hey! Just a few last-minute UI polishes and fixes I wanted to get in: - **Collapsing Toolbar**: Smoothed out the transitions and fixed the grey state not always sticking when navigating between settings screens. - **Multi-Select Dialog**: Gave the checkboxes and search bar a Material 3 makeover and squashed a flicker bug with the checkbox animations. - **Volume Slider**: The tooltip was a bit redundant, so I've hidden it. - **Alerts List**: Added a nice fade-in for the 'No Alerts' message when the list is empty. - **RTL**: Made the back arrow mirror correctly for RTL layouts. - **Cleanup**: Removed some old transition code that wasn't needed anymore.
1 parent 2510a8c commit 235e902

13 files changed

Lines changed: 139 additions & 98 deletions

app/src/main/java/com/red/alert/activities/Main.java

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ public class Main extends AppCompatActivity {
9797
MaterialToolbar mToolbar;
9898
BottomNavigationView mBottomNav;
9999
CollapsingToolbarLayout mCollapsingToolbar;
100+
com.google.android.material.appbar.AppBarLayout mAppBarLayout;
101+
boolean mForceAppBarLifted = false;
100102
OnBackPressedCallback mTabBackCallback;
101103
OnBackPressedCallback mInnerNavCallback;
102104

@@ -182,38 +184,43 @@ public void handleOnBackPressed() {
182184
mInnerNavCallback = new OnBackPressedCallback(false) {
183185
@Override
184186
public void handleOnBackPressed() {
185-
if (!mNavigationStack.isEmpty()) {
186-
// Pop the previous entry
187-
NavigationEntry entry = mNavigationStack.pop();
188-
189-
// Navigate back to the previous fragment
190-
try {
191-
Fragment previousFragment = entry.fragmentClass.newInstance();
187+
// Use popBackStack to trigger the proper pop animations
188+
if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
189+
getSupportFragmentManager().popBackStack();
190+
191+
// Pop our navigation stack too to keep track of titles
192+
if (!mNavigationStack.isEmpty()) {
193+
NavigationEntry entry = mNavigationStack.pop();
192194
updateTitle(entry.title);
195+
}
196+
// Collapse and keep AppBarLayout lifted (grey) during back navigation
197+
if (mAppBarLayout != null) {
198+
mAppBarLayout.setExpanded(false, true); // Collapse with animation
199+
mAppBarLayout.setLiftOnScroll(false);
200+
mAppBarLayout.setLifted(true);
201+
}
193202

194-
getSupportFragmentManager().beginTransaction()
195-
.setReorderingAllowed(true)
196-
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
197-
.replace(R.id.fragment_container, previousFragment)
198-
.commit();
199-
200-
// If stack is now empty, we're at root - disable this callback
201-
// and update UI to hide back button
202-
if (mNavigationStack.isEmpty()) {
203-
setEnabled(false);
204-
if (mToolbar != null) {
205-
mToolbar.setNavigationIcon(null);
206-
mToolbar.setNavigationOnClickListener(null);
207-
}
208-
// Re-enable tab back callback since we are at root
209-
if (mBottomNav != null) {
210-
updateTabBackCallbackState(mBottomNav.getSelectedItemId());
203+
// If back stack is now empty, we're at root - update UI
204+
if (getSupportFragmentManager().getBackStackEntryCount() <= 1) {
205+
// Use a small delay to let the pop complete
206+
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
207+
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
208+
// Re-enable automatic lift-on-scroll at root
209+
mForceAppBarLifted = false;
210+
if (mAppBarLayout != null) {
211+
mAppBarLayout.setLiftOnScroll(true);
212+
}
213+
setEnabled(false);
214+
if (mToolbar != null) {
215+
mToolbar.setNavigationIcon(null);
216+
mToolbar.setNavigationOnClickListener(null);
217+
}
218+
// Re-enable tab back callback since we are at root
219+
if (mBottomNav != null) {
220+
updateTabBackCallbackState(mBottomNav.getSelectedItemId());
221+
}
211222
}
212-
}
213-
} catch (Exception e) {
214-
e.printStackTrace();
215-
// Fallback: just disable and let other callbacks handle it
216-
setEnabled(false);
223+
}, 50);
217224
}
218225
} else {
219226
// Stack is empty, disable and let other callbacks handle it
@@ -281,12 +288,26 @@ public void navigateToFragment(Fragment fragment, String title) {
281288
mInnerNavCallback.setEnabled(true);
282289
}
283290

284-
// Replace fragment WITHOUT adding to back stack
291+
// Replace fragment with slide animations
292+
// Parameters: enter, exit, popEnter, popExit
285293
getSupportFragmentManager().beginTransaction()
286294
.setReorderingAllowed(true)
287-
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
295+
.setCustomAnimations(
296+
R.anim.slide_in_right, // New fragment slides in from right
297+
R.anim.slide_out_left, // Old fragment slides out to left
298+
R.anim.slide_in_left, // When popping: old fragment slides in from left
299+
R.anim.slide_out_right // When popping: current fragment slides out to right
300+
)
288301
.replace(R.id.fragment_container, fragment)
302+
.addToBackStack(null)
289303
.commit();
304+
// Collapse toolbar and force lifted (grey) state during inner navigation
305+
mForceAppBarLifted = true;
306+
if (mAppBarLayout != null) {
307+
mAppBarLayout.setExpanded(false, true); // Collapse with animation
308+
mAppBarLayout.setLiftOnScroll(false);
309+
mAppBarLayout.setLifted(true);
310+
}
290311
}
291312

292313
public void resetToRoot(String title, boolean showImSafe) {
@@ -325,6 +346,9 @@ void initializeUI() {
325346

326347
mImSafe = findViewById(R.id.safe);
327348
mBottomNav = findViewById(R.id.bottom_navigation);
349+
350+
// Get AppBarLayout reference for controlling lifted state during navigation
351+
mAppBarLayout = findViewById(R.id.app_bar);
328352

329353
mBottomNav.setOnItemSelectedListener(new NavigationBarView.OnItemSelectedListener() {
330354
@Override

app/src/main/java/com/red/alert/activities/settings/BasePreferenceFragment.java

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,7 @@ public class BasePreferenceFragment extends PreferenceFragmentCompat
2424
@Override
2525
public void onCreate(@Nullable Bundle savedInstanceState) {
2626
super.onCreate(savedInstanceState);
27-
28-
// Set up native AndroidX transitions for predictive back support
29-
// Enter: slide in from right when navigating forward
30-
androidx.transition.Slide slideEnter = new androidx.transition.Slide(android.view.Gravity.END);
31-
slideEnter.setDuration(300);
32-
setEnterTransition(slideEnter);
33-
34-
// Return: slide out to right when going back
35-
androidx.transition.Slide slideReturn = new androidx.transition.Slide(android.view.Gravity.END);
36-
slideReturn.setDuration(300);
37-
setReturnTransition(slideReturn);
38-
39-
// Exit: fade out when navigating forward (subtle)
40-
androidx.transition.Fade fadeExit = new androidx.transition.Fade();
41-
fadeExit.setDuration(300);
42-
setExitTransition(fadeExit);
43-
44-
// Reenter: fade in when coming back (subtle)
45-
androidx.transition.Fade fadeReenter = new androidx.transition.Fade();
46-
fadeReenter.setDuration(300);
47-
setReenterTransition(fadeReenter);
27+
// Transitions are handled by FragmentTransaction animations in Main.navigateToFragment
4828
}
4929

5030
@Override

app/src/main/java/com/red/alert/ui/elements/SearchableMultiSelectPreference.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
import android.view.inputmethod.InputMethodManager;
1515
import android.widget.ArrayAdapter;
1616
import android.widget.Button;
17-
import android.widget.CheckBox;
1817
import android.widget.CompoundButton;
18+
import com.google.android.material.checkbox.MaterialCheckBox;
1919
import android.widget.EditText;
2020
import android.widget.Filter;
2121
import android.widget.LinearLayout;
@@ -177,29 +177,31 @@ public View getView(final int position, View v, final ViewGroup parent) {
177177
v.setOnClickListener(new View.OnClickListener() {
178178
@Override
179179
public void onClick(View view) {
180-
viewHolder.checkbox.setChecked(!viewHolder.checkbox.isChecked());
180+
// Use performClick to trigger animation
181+
viewHolder.checkbox.performClick();
181182
}
182183
});
183184

184185
viewHolder.checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
185186
@Override
186187
public void onCheckedChanged(CompoundButton compoundButton, boolean val) {
187188
if (mClickedDialogEntryIndices[0]) {
188-
((ListView) parent).setItemChecked(0, false);
189189
mClickedDialogEntryIndices[0] = false;
190190
}
191191

192192
int realPosition = getRealPosition(name);
193-
if (isCheckAllValue(realPosition)) {
193+
boolean isSelectAll = isCheckAllValue(realPosition);
194+
if (isSelectAll) {
194195
checkAll(mDialog, val);
195196
}
196197

197198
mClickedDialogEntryIndices[realPosition] = val;
198-
viewHolder.checkbox.setChecked(val);
199199
item.checked = val;
200200
canCheckAll();
201201

202-
if (mDialog != null) {
202+
// Only invalidate views for Select All (needs to update all checkboxes)
203+
// Don't invalidate for individual items - allows animation to complete
204+
if (isSelectAll && mDialog != null) {
203205
ListView lv = mDialog.findViewById(R.id.searchListView);
204206
if (lv != null) lv.invalidateViews();
205207
}
@@ -333,7 +335,6 @@ private void checkAll(AlertDialog dialog, boolean val) {
333335
if (lv == null) return;
334336
int size = lv.getCount();
335337
for (int i = 0; i < size; i++) {
336-
lv.setItemChecked(i, val);
337338
mClickedDialogEntryIndices[i] = val;
338339
}
339340
}
@@ -398,7 +399,7 @@ protected void onDialogClosed(boolean positiveResult) {
398399
public static class ViewHolder {
399400
public TextView label;
400401
public TextView subLabel;
401-
public CheckBox checkbox;
402+
public MaterialCheckBox checkbox;
402403
}
403404

404405
private static final class ListItemWithIndex implements Comparable {

app/src/main/java/com/red/alert/ui/fragments/AlertsFragment.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,15 @@ void invalidateAlertList() {
496496
}
497497

498498
if (mDisplayAlerts.size() == 0) {
499-
mNoAlerts.setVisibility(View.VISIBLE);
499+
// Fade in the no alerts message smoothly
500+
if (mNoAlerts.getVisibility() != View.VISIBLE) {
501+
mNoAlerts.setAlpha(0f);
502+
mNoAlerts.setVisibility(View.VISIBLE);
503+
mNoAlerts.animate()
504+
.alpha(1f)
505+
.setDuration(200)
506+
.start();
507+
}
500508
} else {
501509
mNoAlerts.setVisibility(View.GONE);
502510
}
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<set xmlns:android="http://schemas.android.com/apk/res/android">
2+
<set xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:interpolator="@android:anim/decelerate_interpolator">
34
<translate
4-
android:duration="350"
5-
android:fromXDelta="-100%"
6-
android:toXDelta="0%"
7-
android:interpolator="@android:anim/accelerate_decelerate_interpolator" />
5+
android:duration="250"
6+
android:fromXDelta="-35%"
7+
android:toXDelta="0%" />
88
<alpha
9-
android:duration="350"
9+
android:duration="200"
1010
android:fromAlpha="0.0"
1111
android:toAlpha="1.0" />
1212
</set>
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<set xmlns:android="http://schemas.android.com/apk/res/android">
2+
<set xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:interpolator="@android:anim/decelerate_interpolator">
34
<translate
4-
android:duration="350"
5-
android:fromXDelta="100%"
6-
android:toXDelta="0%"
7-
android:interpolator="@android:anim/accelerate_decelerate_interpolator" />
5+
android:duration="250"
6+
android:fromXDelta="35%"
7+
android:toXDelta="0%" />
88
<alpha
9-
android:duration="350"
9+
android:duration="200"
1010
android:fromAlpha="0.0"
1111
android:toAlpha="1.0" />
1212
</set>
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<set xmlns:android="http://schemas.android.com/apk/res/android">
2+
<set xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:interpolator="@android:anim/accelerate_interpolator">
34
<translate
4-
android:duration="350"
5+
android:duration="200"
56
android:fromXDelta="0%"
6-
android:toXDelta="-100%"
7-
android:interpolator="@android:anim/accelerate_decelerate_interpolator" />
7+
android:toXDelta="-35%" />
88
<alpha
9-
android:duration="350"
9+
android:duration="80"
1010
android:fromAlpha="1.0"
1111
android:toAlpha="0.0" />
1212
</set>
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<set xmlns:android="http://schemas.android.com/apk/res/android">
2+
<set xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:interpolator="@android:anim/accelerate_interpolator">
34
<translate
4-
android:duration="350"
5+
android:duration="200"
56
android:fromXDelta="0%"
6-
android:toXDelta="100%"
7-
android:interpolator="@android:anim/accelerate_decelerate_interpolator" />
7+
android:toXDelta="35%" />
88
<alpha
9-
android:duration="350"
9+
android:duration="80"
1010
android:fromAlpha="1.0"
1111
android:toAlpha="0.0" />
1212
</set>

app/src/main/res/drawable/ic_arrow_back.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
android:height="24dp"
44
android:viewportWidth="24"
55
android:viewportHeight="24"
6+
android:autoMirrored="true"
67
android:tint="?attr/colorControlNormal">
78
<path
89
android:fillColor="@android:color/white"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="960"
5+
android:viewportHeight="960"
6+
android:tint="?attr/colorControlNormal">
7+
<path
8+
android:fillColor="@android:color/white"
9+
android:pathData="M784,840L532,588Q502,612 463,626Q424,640 380,640Q271,640 195.5,564.5Q120,489 120,380Q120,271 195.5,195.5Q271,120 380,120Q489,120 564.5,195.5Q640,271 640,380Q640,424 626,463Q612,502 588,532L840,784L784,840ZM380,560Q455,560 507.5,507.5Q560,455 560,380Q560,305 507.5,252.5Q455,200 380,200Q305,200 252.5,252.5Q200,305 200,380Q200,455 252.5,507.5Q305,560 380,560Z"/>
10+
</vector>

0 commit comments

Comments
 (0)