Dashboard: Implement horizontal swiping and fix fragment related crashes

This commit is contained in:
Arjan Schrijver 2024-04-08 22:23:34 +02:00
parent 96bbd578c8
commit 6f103b2897
7 changed files with 157 additions and 142 deletions

View File

@ -228,8 +228,7 @@ dependencies {
implementation "androidx.palette:palette:1.0.0"
implementation "androidx.activity:activity:1.7.2"
implementation "androidx.fragment:fragment:1.6.2"
implementation "androidx.navigation:navigation-fragment:2.6.0"
implementation "androidx.navigation:navigation-ui:2.6.0"
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "com.google.android.material:material:1.9.0"
implementation 'com.google.android.flexbox:flexbox:3.0.0'

View File

@ -41,6 +41,7 @@ import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.TypedValue;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
@ -58,11 +59,12 @@ import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.navigation.NavController;
import androidx.navigation.NavGraph;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.bottomnavigation.BottomNavigationView;
@ -85,8 +87,10 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -104,6 +108,9 @@ public class ControlCenterv2 extends AppCompatActivity
= "nodomain.freeyourgadget.gadgetbridge.activities.controlcenter.requestlocationpermissions";
private boolean isLanguageInvalid = false;
private boolean isThemeInvalid = false;
private ViewPager2 viewPager;
private FragmentStateAdapter pagerAdapter;
private SwipeRefreshLayout swipeLayout;
private static PhoneStateListener fakeStateListener;
//needed for KK compatibility
@ -134,6 +141,12 @@ public class ControlCenterv2 extends AppCompatActivity
case ACTION_REQUEST_LOCATION_PERMISSIONS:
checkAndRequestLocationPermissions();
break;
case GBDevice.ACTION_DEVICE_CHANGED:
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev != null && !dev.isBusy()) {
swipeLayout.setRefreshing(false);
}
break;
}
}
};
@ -165,6 +178,7 @@ public class ControlCenterv2 extends AppCompatActivity
Prefs prefs = GBApplication.getPrefs();
// Determine availability of device with activity tracking functionality
boolean activityTrackerAvailable = false;
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
for (GBDevice dev : devices) {
@ -174,21 +188,26 @@ public class ControlCenterv2 extends AppCompatActivity
}
}
NavHostFragment navHostFragment = (NavHostFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
NavController navController = navHostFragment.getNavController();
if (!prefs.getBoolean("dashboard_as_default_view", true) || !activityTrackerAvailable) {
NavGraph navGraph = navController.getNavInflater().inflate(R.navigation.main);
navGraph.setStartDestination(R.id.bottom_nav_devices);
navController.setGraph(navGraph);
}
BottomNavigationView navigationView = findViewById(R.id.bottom_nav_bar);
NavigationUI.setupWithNavController(navigationView, navController);
navigationView.setVisibility(activityTrackerAvailable ? View.VISIBLE : View.GONE);
// Initialize drawer
NavigationView drawerNavigationView = findViewById(R.id.nav_view);
drawerNavigationView.setNavigationItemSelectedListener(this);
// Initialize bottom navigation
BottomNavigationView navigationView = findViewById(R.id.bottom_nav_bar);
navigationView.setVisibility(activityTrackerAvailable ? View.VISIBLE : View.GONE);
navigationView.setOnItemSelectedListener(menuItem -> {
switch (menuItem.getItemId()) {
case R.id.bottom_nav_dashboard:
viewPager.setCurrentItem(0, true);
break;
case R.id.bottom_nav_devices:
viewPager.setCurrentItem(1, true);
break;
}
return true;
});
// Initialize actionbar
MaterialToolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
DrawerLayout drawer = findViewById(R.id.drawer_layout);
@ -196,7 +215,6 @@ public class ControlCenterv2 extends AppCompatActivity
this, drawer, toolbar, R.string.controlcenter_navigation_drawer_open, R.string.controlcenter_navigation_drawer_close);
drawer.setDrawerListener(toggle);
toggle.syncState();
if (GBApplication.areDynamicColorsEnabled()) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = getTheme();
@ -208,6 +226,53 @@ public class ControlCenterv2 extends AppCompatActivity
toolbar.setTitleTextColor(getResources().getColor(android.R.color.white));
}
// Configure ViewPager2 with fragment adapter and default fragment
viewPager = findViewById(R.id.dashboard_viewpager);
pagerAdapter = new MainFragmentsPagerAdapter(this);
viewPager.setAdapter(pagerAdapter);
if (!prefs.getBoolean("dashboard_as_default_view", true) || !activityTrackerAvailable) {
viewPager.setCurrentItem(1, false);
navigationView.getMenu().getItem(1).setChecked(true);
}
// Sync ViewPager changes with BottomNavigationView
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
navigationView.getMenu().getItem(position).setChecked(true);
}
});
// Make sure the SwipeRefreshLayout doesn't interfere with the ViewPager2
viewPager.getChildAt(viewPager.getCurrentItem()).setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
swipeLayout.setEnabled(false);
} else {
swipeLayout.setEnabled(true);
}
return false;
});
// Set pull-down-to-refresh action
swipeLayout = findViewById(R.id.dashboard_swipe_layout);
swipeLayout.setOnRefreshListener(() -> {
// Signal DeviceCommunicationService to fetch activity for all connected devices
Intent intent = new Intent(getApplicationContext(), DeviceCommunicationService.class);
intent.setAction(DeviceService.ACTION_FETCH_RECORDED_DATA)
.putExtra(DeviceService.EXTRA_RECORDED_DATA_TYPES, ActivityKind.TYPE_ACTIVITY);
startService(intent);
// Hide 'refreshing' animation immediately if no health devices are connected
List<GBDevice> devices1 = GBApplication.app().getDeviceManager().getDevices();
for (GBDevice dev : devices1) {
if (dev.getDeviceCoordinator().supportsActivityTracking() && dev.isInitialized()) {
return;
}
}
swipeLayout.setRefreshing(false);
GB.toast(getString(R.string.info_no_devices_connected), Toast.LENGTH_LONG, GB.WARN);
});
// Set up local intent listener
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
filterLocal.addAction(GBApplication.ACTION_THEME_CHANGE);
@ -215,6 +280,7 @@ public class ControlCenterv2 extends AppCompatActivity
filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
filterLocal.addAction(ACTION_REQUEST_PERMISSIONS);
filterLocal.addAction(ACTION_REQUEST_LOCATION_PERMISSIONS);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
/*
@ -670,4 +736,24 @@ public class ControlCenterv2 extends AppCompatActivity
return builder.create();
}
}
private class MainFragmentsPagerAdapter extends FragmentStateAdapter {
public MainFragmentsPagerAdapter(FragmentActivity fa) {
super(fa);
}
@Override
public Fragment createFragment(int position) {
if (position == 0) {
return new DashboardFragment();
} else {
return new DevicesFragment();
}
}
@Override
public int getItemCount() {
return 2;
}
}
}

View File

@ -29,7 +29,6 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -37,7 +36,6 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentContainerView;
import androidx.gridlayout.widget.GridLayout;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.card.MaterialCardView;
@ -65,12 +63,8 @@ import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardSleepW
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStepsWidget;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardTodayWidget;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.util.DashboardUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class DashboardFragment extends Fragment {
@ -81,7 +75,6 @@ public class DashboardFragment extends Fragment {
private TextView arrowLeft;
private TextView arrowRight;
private GridLayout gridLayout;
private SwipeRefreshLayout swipeLayout;
private DashboardTodayWidget todayWidget;
private DashboardGoalsWidget goalsWidget;
private DashboardStepsWidget stepsWidget;
@ -119,26 +112,6 @@ public class DashboardFragment extends Fragment {
setHasOptionsMenu(true);
textViewDate = dashboardView.findViewById(R.id.dashboard_date);
gridLayout = dashboardView.findViewById(R.id.dashboard_gridlayout);
swipeLayout = dashboardView.findViewById(R.id.dashboard_swipe_layout);
swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
// Signal DeviceCommunicationService to fetch activity for all connected devices
Intent intent = new Intent(requireContext(), DeviceCommunicationService.class);
intent.setAction(DeviceService.ACTION_FETCH_RECORDED_DATA)
.putExtra(DeviceService.EXTRA_RECORDED_DATA_TYPES, ActivityKind.TYPE_ACTIVITY);
requireContext().startService(intent);
// Hide 'refreshing' animation immediately if no health devices are connected
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
for (GBDevice dev : devices) {
if (dev.getDeviceCoordinator().supportsActivityTracking() && dev.isInitialized()) {
return;
}
}
swipeLayout.setRefreshing(false);
GB.toast(getString(R.string.info_no_devices_connected), Toast.LENGTH_LONG, GB.WARN);
}
});
// Increase column count on landscape, tablets and open foldables
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
@ -164,14 +137,6 @@ public class DashboardFragment extends Fragment {
dashboardData = (DashboardData) savedInstanceState.getSerializable("dashboard_data");
}
// Make sure the widget fragments are (re)instantiated when drawing the dashboard
todayWidget = null;
goalsWidget = null;
stepsWidget = null;
distanceWidget = null;
activeTimeWidget = null;
sleepWidget = null;
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
filterLocal.addAction(ACTION_CONFIG_CHANGE);
@ -183,7 +148,6 @@ public class DashboardFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
draw();
if (isConfigChanged) {
isConfigChanged = false;
fullRefresh();
@ -246,7 +210,6 @@ public class DashboardFragment extends Fragment {
}
private void refresh() {
swipeLayout.setRefreshing(false);
day.set(Calendar.HOUR_OF_DAY, 23);
day.set(Calendar.MINUTE, 59);
day.set(Calendar.SECOND, 59);
@ -336,7 +299,7 @@ public class DashboardFragment extends Fragment {
FragmentContainerView fragment = new FragmentContainerView(requireActivity());
int fragmentId = View.generateViewId();
fragment.setId(fragmentId);
getParentFragmentManager()
getChildFragmentManager()
.beginTransaction()
.replace(fragmentId, widgetObj)
.commit();

View File

@ -22,15 +22,18 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/main"
app:defaultNavHost="true"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/dashboard_swipe_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/appbarlayout"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" />
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/dashboard_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav_bar"

View File

@ -1,65 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activities.DashboardFragment">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/dashboard_swipe_layout"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="horizontal">
<Button
android:id="@+id/arrow_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:text="\u003C"
android:textStyle="bold"
android:textSize="20sp"
android:layout_alignParentLeft="true" />
<TextView
android:id="@+id/dashboard_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/activity_summary_today"
android:textStyle="bold"
android:textSize="25sp"
android:layout_centerInParent="true"/>
<Button
android:id="@+id/arrow_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:text="\u003E"
android:textStyle="bold"
android:textSize="20sp"
android:layout_alignParentRight="true" />
</RelativeLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
<androidx.gridlayout.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="horizontal">
<Button
android:id="@+id/arrow_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:text="\u003C"
android:textStyle="bold"
android:textSize="20sp"
android:layout_alignParentLeft="true" />
<TextView
android:id="@+id/dashboard_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/activity_summary_today"
android:textStyle="bold"
android:textSize="25sp"
android:layout_centerInParent="true"/>
<Button
android:id="@+id/arrow_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:text="\u003E"
android:textStyle="bold"
android:textSize="20sp"
android:layout_alignParentRight="true" />
</RelativeLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.gridlayout.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/dashboard_gridlayout"
app:columnCount="2" />
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</RelativeLayout>
android:id="@+id/dashboard_gridlayout"
app:columnCount="2" />
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content_main"

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
app:startDestination="@id/bottom_nav_dashboard">
<fragment
android:id="@+id/bottom_nav_dashboard"
android:name="nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment"
android:label="fragment_dashboard"
tools:layout="@layout/fragment_dashboard">
<action
android:id="@+id/dashboard_to_devices"
app:destination="@id/bottom_nav_devices" />
</fragment>
<fragment
android:id="@+id/bottom_nav_devices"
android:name="nodomain.freeyourgadget.gadgetbridge.activities.DevicesFragment"
android:label="fragment_devices"
tools:layout="@layout/fragment_devices">
<action
android:id="@+id/devices_to_dashboard"
app:destination="@id/bottom_nav_dashboard" />
</fragment>
</navigation>