Skip to content

Commit

Permalink
Support swipe gesture to switch between tabs (#308)
Browse files Browse the repository at this point in the history
* Support swipe gesture to switch between tabs

* Use tab layout for tab indicators
  • Loading branch information
frimtec authored Apr 25, 2022
1 parent 0dc6e03 commit 9d5cd09
Show file tree
Hide file tree
Showing 14 changed files with 178 additions and 140 deletions.
11 changes: 11 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.github.frimtec.android.pikettassist.ui;

public enum FragmentName {
public enum FragmentPosition {
STATE,
SHIFTS,
ALERT_LOG,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;

import com.android.billingclient.api.BillingClient;
import com.github.frimtec.android.pikettassist.R;
Expand All @@ -43,6 +46,7 @@
import com.github.frimtec.android.pikettassist.ui.about.AboutActivity;
import com.github.frimtec.android.pikettassist.ui.alerts.AlertListFragment;
import com.github.frimtec.android.pikettassist.ui.common.AbstractListFragment;
import com.github.frimtec.android.pikettassist.ui.common.ViewPager2Helper;
import com.github.frimtec.android.pikettassist.ui.overview.StateFragment;
import com.github.frimtec.android.pikettassist.ui.settings.SettingsActivity;
import com.github.frimtec.android.pikettassist.ui.shifts.ShiftListFragment;
Expand All @@ -51,51 +55,41 @@
import com.github.frimtec.android.securesmsproxyapi.SecureSmsProxyFacade;
import com.github.frimtec.android.securesmsproxyapi.SecureSmsProxyFacade.Installation;
import com.github.frimtec.android.securesmsproxyapi.SecureSmsProxyFacade.RegistrationResult;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";

private static final String ACTIVE_FRAGMENT_STATE = "ACTIVE_FRAGMENT";
private BroadcastReceiver broadcastReceiver;
private StateFragment stateFragment;
private ShiftListFragment shiftListFragment;
private AlertListFragment alertListFragment;
private TestAlarmFragment testAlarmFragment;
private AbstractListFragment<?> activeFragment;
private SecureSmsProxyFacade s2msp;

private DonationFragment donationFragment;
private BillingAdapter billingAdapter;
public static final int[] TAB_ICONS = new int[]{
R.drawable.ic_home_black_24dp,
R.drawable.ic_date_range_black_24dp,
R.drawable.ic_siren,
R.drawable.ic_test_alarm
};

private static final Map<FragmentName, Integer> FRAGMENT_BUTTON_ID_MAP;
class SwipeFragmentStateAdapter extends FragmentStateAdapter {

@SuppressLint("UseSparseArrays")
private static final Map<Integer, FragmentName> BUTTON_ID_FRAGMENT_MAP = new HashMap<>();
private final FragmentActivity fragmentActivity;

static {
FRAGMENT_BUTTON_ID_MAP = new EnumMap<>(FragmentName.class);
FRAGMENT_BUTTON_ID_MAP.put(FragmentName.STATE, R.id.navigation_home);
FRAGMENT_BUTTON_ID_MAP.put(FragmentName.SHIFTS, R.id.navigation_shifts);
FRAGMENT_BUTTON_ID_MAP.put(FragmentName.ALERT_LOG, R.id.navigation_alert_log);
FRAGMENT_BUTTON_ID_MAP.put(FragmentName.TEST_ALARMS, R.id.navigation_test_alarms);

FRAGMENT_BUTTON_ID_MAP.forEach((fragment, buttonId) -> BUTTON_ID_FRAGMENT_MAP.put(buttonId, fragment));
}
public SwipeFragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
this.fragmentActivity = fragmentActivity;
}

private void loadFragment(FragmentName fragment) {
switch (fragment) {
case STATE:
if (stateFragment == null) {
stateFragment = new StateFragment();
stateFragment.setActivityFacade(this, new StateFragment.BillingAccess() {
@NonNull
@Override
public Fragment createFragment(int position) {
FragmentPosition fragmentPosition = ensureValidFragmentPosition(position);
switch (fragmentPosition) {
case STATE:
StateFragment stateFragment = new StateFragment();
stateFragment.setActivityFacade(MainActivity.this, new StateFragment.BillingAccess() {
@Override
public List<BillingProvider.BillingState> getProducts() {
return MainActivity.this.billingAdapter.getAllProducts();
Expand All @@ -106,34 +100,43 @@ public void showDonationDialog() {
MainActivity.this.showDonationDialog();
}
});
}
activeFragment = stateFragment;
break;
case SHIFTS:
if (shiftListFragment == null) {
shiftListFragment = new ShiftListFragment();
}
activeFragment = shiftListFragment;
break;
case ALERT_LOG:
if (alertListFragment == null) {
alertListFragment = new AlertListFragment();
}
activeFragment = alertListFragment;
break;
case TEST_ALARMS:
if (testAlarmFragment == null) {
testAlarmFragment = new TestAlarmFragment();
}
activeFragment = testAlarmFragment;
break;
default:
throw new IllegalStateException("Unknown fragment: " + fragment);
return stateFragment;
case SHIFTS:
return new ShiftListFragment();
case ALERT_LOG:
return new AlertListFragment();
case TEST_ALARMS:
return new TestAlarmFragment();
}
throw new IllegalStateException("Unknown fragment: " + fragmentPosition);
}
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fm.beginTransaction();
fragmentTransaction.replace(R.id.frame_layout, activeFragment);
fragmentTransaction.commit();

@Override
public int getItemCount() {
return ApplicationPreferences.instance().getTestAlarmEnabled(fragmentActivity.getApplicationContext()) ? 4 : 3;
}
}

private ViewPager2 viewPager;

private BroadcastReceiver broadcastReceiver;
private SecureSmsProxyFacade s2msp;

private DonationFragment donationFragment;
private BillingAdapter billingAdapter;

private TabLayoutMediator tabLayoutMediator;

private void loadFragment(FragmentPosition fragmentPosition) {
viewPager.setCurrentItem(fragmentPosition.ordinal(), false);
}

@NonNull
private static FragmentPosition ensureValidFragmentPosition(int fragmentPosition) {
if (fragmentPosition >= FragmentPosition.values().length) {
throw new IllegalStateException("Unknown fragment position: " + fragmentPosition);
}
return FragmentPosition.values()[fragmentPosition];
}

@Override
Expand All @@ -147,27 +150,24 @@ protected void onCreate(Bundle savedInstanceState) {

setContentView(R.layout.activity_main);

BottomNavigationView navigation = findViewById(R.id.navigation);
navigation.setOnItemSelectedListener(item -> {
FragmentName fragment = BUTTON_ID_FRAGMENT_MAP.get(item.getItemId());
if (fragment != null) {
loadFragment(fragment);
return true;
}
return false;
});
viewPager = findViewById(R.id.view_pager);
viewPager.setAdapter(new SwipeFragmentStateAdapter(this));
ViewPager2Helper.reduceDragSensitivity(viewPager, 8);

TabLayout tabLayout = findViewById(R.id.tab_layout);
tabLayoutMediator = new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> tab.setIcon(TAB_ICONS[position]));
tabLayoutMediator.attach();

this.billingAdapter = new BillingAdapter(this);

FragmentName savedFragmentName = FragmentName.STATE;
FragmentPosition savedFragmentPosition = FragmentPosition.STATE;
if (savedInstanceState != null) {
savedFragmentName = FragmentName.valueOf(savedInstanceState.getString(ACTIVE_FRAGMENT_STATE, savedFragmentName.name()));
savedFragmentPosition = ensureValidFragmentPosition(savedInstanceState.getInt(ACTIVE_FRAGMENT_STATE, savedFragmentPosition.ordinal()));
} else {
// register on new app start only, not on orientation change
registerOnSmsAdapter();
}
loadFragment(savedFragmentName);
updateBottomNavigation();
loadFragment(savedFragmentPosition);
PikettService.enqueueWork(this);
}

Expand All @@ -181,15 +181,19 @@ private void registerOnSmsAdapter() {
}

private void refresh() {
refreshTabLabels();
FragmentManager fm = getSupportFragmentManager();
var activeFragment = (AbstractListFragment<?>) fm.findFragmentByTag("f" + viewPager.getCurrentItem());
if (activeFragment != null) {
activeFragment.refresh();
}
}

private void updateBottomNavigation() {
BottomNavigationView navigation = findViewById(R.id.navigation);
MenuItem item = navigation.getMenu().findItem(R.id.navigation_test_alarms);
item.setVisible(ApplicationPreferences.instance().getTestAlarmEnabled(this));
private void refreshTabLabels() {
if (tabLayoutMediator != null) {
tabLayoutMediator.detach();
tabLayoutMediator.attach();
}
}

@Override
Expand All @@ -202,7 +206,6 @@ protected void onPause() {
protected void onResume() {
super.onResume();
registerBroadcastReceiver();
updateBottomNavigation();
BillingManager billingManager = this.billingAdapter.getBillingManager();
if (billingManager != null && billingManager.getBillingClientResponseCode() == BillingResponseCode.OK) {
billingManager.queryPurchases();
Expand Down Expand Up @@ -308,7 +311,7 @@ private boolean isAcquireFragmentShown() {
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(ACTIVE_FRAGMENT_STATE, this.activeFragment.getFragmentName().name());
outState.putInt(ACTIVE_FRAGMENT_STATE, this.viewPager.getCurrentItem());
}

@Override
Expand Down Expand Up @@ -347,4 +350,5 @@ private void unregisterBroadcastReceiver() {
broadcastReceiver = null;
}
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.frimtec.android.pikettassist.ui.alerts;

import static com.github.frimtec.android.pikettassist.ui.FragmentName.ALERT_LOG;
import static java.time.temporal.ChronoUnit.DAYS;

import android.annotation.SuppressLint;
Expand Down Expand Up @@ -69,7 +68,6 @@ public AlertListFragment() {

@SuppressLint("ValidFragment")
AlertListFragment(AlertDao alertDao) {
super(ALERT_LOG);
this.alertDao = alertDao;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,15 @@
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;

import com.github.frimtec.android.pikettassist.R;
import com.github.frimtec.android.pikettassist.ui.FragmentName;
import com.google.android.material.floatingactionbutton.FloatingActionButton;

import java.util.Optional;

public abstract class AbstractListFragment<T> extends Fragment {

private final FragmentName fragmentName;
private ListView listView;

protected AbstractListFragment(FragmentName fragmentName) {
this.fragmentName = fragmentName;
}

public FragmentName getFragmentName() {
return fragmentName;
protected AbstractListFragment() {
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.github.frimtec.android.pikettassist.ui.common;

import android.util.Log;

import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;

import java.lang.reflect.Field;

public class ViewPager2Helper {

private static final String TAG = "ViewPager2Helper";

public static void reduceDragSensitivity(ViewPager2 viewPager, int sensitivity) {
try {
RecyclerView recyclerView = (RecyclerView) getRecyclerViewField().get(viewPager);

Field touchSlopField = getTouchSlopField();
//noinspection ConstantConditions
int touchSlop = (int) touchSlopField.get(recyclerView);
touchSlopField.set(recyclerView, touchSlop * sensitivity);
} catch (NoSuchFieldException|IllegalAccessException e) {
Log.e(TAG, "Cannot change drag sensibility", e);
}
}

static Field getRecyclerViewField() throws NoSuchFieldException {
Field field = ViewPager2.class.getDeclaredField("mRecyclerView");
field.setAccessible(true);
return field;
}

static Field getTouchSlopField() throws NoSuchFieldException {
Field field = RecyclerView.class.getDeclaredField("mTouchSlop");
field.setAccessible(true);
return field;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
import com.github.frimtec.android.pikettassist.service.system.SignalStrengthService;
import com.github.frimtec.android.pikettassist.state.ApplicationPreferences;
import com.github.frimtec.android.pikettassist.state.ApplicationState;
import com.github.frimtec.android.pikettassist.ui.FragmentName;
import com.github.frimtec.android.pikettassist.ui.common.AbstractListFragment;
import com.github.frimtec.android.securesmsproxyapi.SecureSmsProxyFacade;
import com.github.frimtec.android.securesmsproxyapi.SecureSmsProxyFacade.Installation;
Expand Down Expand Up @@ -108,7 +107,6 @@ public StateFragment() {

@SuppressLint("ValidFragment")
StateFragment(AlertDao alertDao, TestAlarmDao testAlarmDao) {
super(FragmentName.STATE);
this.alertDao = alertDao;
this.testAlarmDao = testAlarmDao;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.github.frimtec.android.pikettassist.ui.shifts;

import static com.github.frimtec.android.pikettassist.service.system.Feature.PERMISSION_CALENDAR_READ;
import static com.github.frimtec.android.pikettassist.ui.FragmentName.SHIFTS;
import static com.github.frimtec.android.pikettassist.ui.common.DurationFormatter.UnitNameProvider.translatedFormatter;
import static com.github.frimtec.android.pikettassist.ui.common.DurationFormatter.toDurationString;

Expand Down Expand Up @@ -32,10 +31,6 @@ public class ShiftListFragment extends AbstractListFragment<Shift> {

private View headerView;

public ShiftListFragment() {
super(SHIFTS);
}

@Override
protected void configureListView(ListView listView) {
listView.setClickable(true);
Expand Down
Loading

0 comments on commit 9d5cd09

Please sign in to comment.