From b6cc5513db02d941f0c6fa3c5df9ff5095c0c84b Mon Sep 17 00:00:00 2001 From: Christophe Beyls Date: Wed, 24 Jun 2015 13:17:58 +0200 Subject: [PATCH] Replaced the ListView main menu with a ScrollView and an AdapterLinearLayout. It makes the layout simpler and fixes focus issues in keypad navigation mode. --- .../fosdem/activities/MainActivity.java | 114 ++++++++-------- .../fosdem/widgets/AdapterLinearLayout.java | 123 ++++++++++++++++++ .../res/drawable-v21/menu_item_background.xml | 13 ++ .../res/drawable/menu_item_background.xml | 12 ++ app/src/main/res/layout/footer_main_menu.xml | 24 ---- app/src/main/res/layout/header_main_menu.xml | 7 - app/src/main/res/layout/main.xml | 46 +++++-- app/src/main/res/values/styles.xml | 2 + 8 files changed, 242 insertions(+), 99 deletions(-) create mode 100644 app/src/main/java/be/digitalia/fosdem/widgets/AdapterLinearLayout.java create mode 100644 app/src/main/res/drawable-v21/menu_item_background.xml create mode 100644 app/src/main/res/drawable/menu_item_background.xml delete mode 100644 app/src/main/res/layout/footer_main_menu.xml delete mode 100644 app/src/main/res/layout/header_main_menu.xml diff --git a/app/src/main/java/be/digitalia/fosdem/activities/MainActivity.java b/app/src/main/java/be/digitalia/fosdem/activities/MainActivity.java index 3ad8e9a..6095b4e 100644 --- a/app/src/main/java/be/digitalia/fosdem/activities/MainActivity.java +++ b/app/src/main/java/be/digitalia/fosdem/activities/MainActivity.java @@ -43,10 +43,8 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.ListView; import android.widget.ProgressBar; +import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; @@ -62,13 +60,14 @@ import be.digitalia.fosdem.fragments.LiveFragment; import be.digitalia.fosdem.fragments.MapFragment; import be.digitalia.fosdem.fragments.PersonsListFragment; import be.digitalia.fosdem.fragments.TracksFragment; +import be.digitalia.fosdem.widgets.AdapterLinearLayout; /** * Main entry point of the application. Allows to switch between section fragments and update the database. * * @author Christophe Beyls */ -public class MainActivity extends ActionBarActivity implements ListView.OnItemClickListener { +public class MainActivity extends ActionBarActivity { private enum Section { TRACKS(TracksFragment.class, R.string.menu_tracks, R.drawable.ic_event_grey600_24dp, true), @@ -225,23 +224,16 @@ public class MainActivity extends ActionBarActivity implements ListView.OnItemCl // Setup Main menu mainMenu = findViewById(R.id.main_menu); - ListView menuListView = (ListView) findViewById(R.id.main_menu_list); - LayoutInflater inflater = LayoutInflater.from(this); - View menuHeaderView = inflater.inflate(R.layout.header_main_menu, null); - menuListView.addHeaderView(menuHeaderView, null, false); - View menuFooterView = inflater.inflate(R.layout.footer_main_menu, null); - menuFooterView.findViewById(R.id.settings).setOnClickListener(menuFooterClickListener); - menuFooterView.findViewById(R.id.about).setOnClickListener(menuFooterClickListener); - menuListView.addFooterView(menuFooterView, null, false); + final AdapterLinearLayout sectionsList = (AdapterLinearLayout) findViewById(R.id.sections); + menuAdapter = new MainMenuAdapter(getLayoutInflater()); + sectionsList.setAdapter(menuAdapter); + mainMenu.findViewById(R.id.settings).setOnClickListener(menuFooterClickListener); + mainMenu.findViewById(R.id.about).setOnClickListener(menuFooterClickListener); LocalBroadcastManager.getInstance(this).registerReceiver(scheduleRefreshedReceiver, new IntentFilter(DatabaseManager.ACTION_SCHEDULE_REFRESHED)); - menuAdapter = new MainMenuAdapter(inflater); - menuListView.setAdapter(menuAdapter); - menuListView.setOnItemClickListener(this); - - // Last update date, below the menu - lastUpdateTextView = (TextView) findViewById(R.id.last_update); + // Last update date, below the list + lastUpdateTextView = (TextView) mainMenu.findViewById(R.id.last_update); updateLastUpdateTime(); // Restore current section @@ -254,7 +246,18 @@ public class MainActivity extends ActionBarActivity implements ListView.OnItemCl currentSection = Section.values()[savedInstanceState.getInt(STATE_CURRENT_SECTION)]; } // Ensure the current section is visible in the menu - menuListView.setSelection(currentSection.ordinal()); + sectionsList.post(new Runnable() { + @Override + public void run() { + if (sectionsList.getChildCount() > currentSection.ordinal()) { + ScrollView mainMenuScrollView = (ScrollView) findViewById(R.id.main_menu_scroll); + int requiredScroll = sectionsList.getTop() + + sectionsList.getChildAt(currentSection.ordinal()).getBottom() + - mainMenuScrollView.getHeight(); + mainMenuScrollView.scrollTo(0, Math.max(0, requiredScroll)); + } + } + }); updateActionBar(); } @@ -452,12 +455,11 @@ public class MainActivity extends ActionBarActivity implements ListView.OnItemCl } }; - private class MainMenuAdapter extends BaseAdapter { + private class MainMenuAdapter extends AdapterLinearLayout.Adapter
{ private final Section[] sections = Section.values(); private final LayoutInflater inflater; private final int currentSectionForegroundColor; - private final int currentSectionBackgroundColor; public MainMenuAdapter(LayoutInflater inflater) { this.inflater = inflater; @@ -468,7 +470,6 @@ public class MainActivity extends ActionBarActivity implements ListView.OnItemCl } finally { a.recycle(); } - currentSectionBackgroundColor = getResources().getColor(R.color.translucent_grey); } @Override @@ -481,72 +482,65 @@ public class MainActivity extends ActionBarActivity implements ListView.OnItemCl return sections[position]; } - @Override - public long getItemId(int position) { - return position; - } - @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = inflater.inflate(R.layout.item_main_menu, parent, false); + convertView.setOnClickListener(sectionClickListener); } Section section = getItem(position); + convertView.setSelected(section == currentSection); TextView tv = (TextView) convertView.findViewById(R.id.section_text); SpannableString sectionTitle = new SpannableString(getString(section.getTitleResId())); Drawable sectionIcon = getResources().getDrawable(section.getIconResId()); - int backgroundColor; if (section == currentSection) { // Special color for the current section //sectionTitle.setSpan(new StyleSpan(Typeface.BOLD), 0, sectionTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); sectionTitle.setSpan(new ForegroundColorSpan(currentSectionForegroundColor), 0, sectionTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // We need to mutate the drawable before applying the ColorFilter, or else all the similar drawable instances will be tinted. sectionIcon.mutate().setColorFilter(currentSectionForegroundColor, PorterDuff.Mode.SRC_IN); - backgroundColor = currentSectionBackgroundColor; - } else { - backgroundColor = Color.TRANSPARENT; } tv.setText(sectionTitle); tv.setCompoundDrawablesWithIntrinsicBounds(sectionIcon, null, null, null); - tv.setBackgroundColor(backgroundColor); return convertView; } } - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - // Decrease position by 1 since the listView has a header view. - Section section = menuAdapter.getItem(position - 1); - if (section != currentSection) { - // Switch to new section - FragmentManager fm = getSupportFragmentManager(); - FragmentTransaction ft = fm.beginTransaction().setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); - Fragment f = fm.findFragmentById(R.id.content); - if (f != null) { - if (currentSection.shouldKeep()) { - ft.detach(f); - } else { - ft.remove(f); + private final View.OnClickListener sectionClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + Section section = menuAdapter.getItem(((ViewGroup) view.getParent()).indexOfChild(view)); + if (section != currentSection) { + // Switch to new section + FragmentManager fm = getSupportFragmentManager(); + FragmentTransaction ft = fm.beginTransaction().setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + Fragment f = fm.findFragmentById(R.id.content); + if (f != null) { + if (currentSection.shouldKeep()) { + ft.detach(f); + } else { + ft.remove(f); + } } - } - String fragmentClassName = section.getFragmentClassName(); - if (section.shouldKeep() && ((f = fm.findFragmentByTag(fragmentClassName)) != null)) { - ft.attach(f); - } else { - f = Fragment.instantiate(this, fragmentClassName); - ft.add(R.id.content, f, fragmentClassName); - } - ft.commit(); + String fragmentClassName = section.getFragmentClassName(); + if (section.shouldKeep() && ((f = fm.findFragmentByTag(fragmentClassName)) != null)) { + ft.attach(f); + } else { + f = Fragment.instantiate(MainActivity.this, fragmentClassName); + ft.add(R.id.content, f, fragmentClassName); + } + ft.commit(); - currentSection = section; - menuAdapter.notifyDataSetChanged(); + currentSection = section; + menuAdapter.notifyDataSetChanged(); + } + + drawerLayout.closeDrawer(mainMenu); } - - drawerLayout.closeDrawer(mainMenu); - } + }; public static class AboutDialogFragment extends DialogFragment { diff --git a/app/src/main/java/be/digitalia/fosdem/widgets/AdapterLinearLayout.java b/app/src/main/java/be/digitalia/fosdem/widgets/AdapterLinearLayout.java new file mode 100644 index 0000000..8e64836 --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/widgets/AdapterLinearLayout.java @@ -0,0 +1,123 @@ +package be.digitalia.fosdem.widgets; + +import android.content.Context; +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +/** + * Vertical LinearLayout populated by a special adapter. + * + * @author Christophe Beyls + */ +public class AdapterLinearLayout extends LinearLayout { + + /** + * Implement this Adapter to populate the layout. + * Call notifyDataSetChanged() to update it. + */ + public static abstract class Adapter { + + private final DataSetObservable mDataSetObservable = new DataSetObservable(); + + private void registerDataSetObserver(DataSetObserver observer) { + mDataSetObservable.registerObserver(observer); + } + + private void unregisterDataSetObserver(DataSetObserver observer) { + mDataSetObservable.unregisterObserver(observer); + } + + public void notifyDataSetChanged() { + mDataSetObservable.notifyChanged(); + } + + public abstract int getCount(); + + public abstract T getItem(int position); + + public abstract View getView(int position, View convertView, ViewGroup parent); + } + + private class AdapterLinearLayoutDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + populateFromAdapter(); + } + } + + private Adapter mAdapter; + private AdapterLinearLayoutDataSetObserver mDataSetObserver; + + + public AdapterLinearLayout(Context context) { + this(context, null); + } + + public AdapterLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + setOrientation(VERTICAL); + } + + public void setAdapter(Adapter adapter) { + if (mAdapter == adapter) { + return; + } + if (mAdapter != null && mDataSetObserver != null) { + mAdapter.unregisterDataSetObserver(mDataSetObserver); + } + removeAllViews(); + mAdapter = adapter; + if (adapter != null && mDataSetObserver != null) { + populateFromAdapter(); + adapter.registerDataSetObserver(mDataSetObserver); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mDataSetObserver = new AdapterLinearLayoutDataSetObserver(); + if (mAdapter != null) { + populateFromAdapter(); + mAdapter.registerDataSetObserver(mDataSetObserver); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mDataSetObserver); + } + mDataSetObserver = null; + } + + private void populateFromAdapter() { + final Adapter adapter = mAdapter; + final int currentCount = getChildCount(); + final int newCount = adapter.getCount(); + final int commonCount = Math.min(currentCount, newCount); + // 1. Update common views + for (int i = 0; i < commonCount; ++i) { + final View currentView = getChildAt(i); + final View newView = adapter.getView(i, currentView, this); + if (currentView != newView) { + // Edge case: View is not recycled + removeViewAt(i); + addView(newView, i); + } + } + // 2a. Add missing views + for (int i = commonCount; i < newCount; ++i) { + addView(adapter.getView(i, null, this)); + } + // 2b. Remove extra views (starting from the end to avoid array copies) + for (int i = currentCount - 1; i >= commonCount; --i) { + removeViewAt(i); + } + } +} diff --git a/app/src/main/res/drawable-v21/menu_item_background.xml b/app/src/main/res/drawable-v21/menu_item_background.xml new file mode 100644 index 0000000..c0e2348 --- /dev/null +++ b/app/src/main/res/drawable-v21/menu_item_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/menu_item_background.xml b/app/src/main/res/drawable/menu_item_background.xml new file mode 100644 index 0000000..099ece6 --- /dev/null +++ b/app/src/main/res/drawable/menu_item_background.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/footer_main_menu.xml b/app/src/main/res/layout/footer_main_menu.xml deleted file mode 100644 index d703471..0000000 --- a/app/src/main/res/layout/footer_main_menu.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/header_main_menu.xml b/app/src/main/res/layout/header_main_menu.xml deleted file mode 100644 index 8018f2c..0000000 --- a/app/src/main/res/layout/header_main_menu.xml +++ /dev/null @@ -1,7 +0,0 @@ - - diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml index 7320509..170da89 100644 --- a/app/src/main/res/layout/main.xml +++ b/app/src/main/res/layout/main.xml @@ -35,19 +35,49 @@ android:layout_width="260dp" android:layout_height="match_parent" android:layout_gravity="left" - android:background="@color/main_menu_background" android:orientation="vertical"> - + android:background="@color/main_menu_background"> + + + + + + + + + + + + + + +