diff --git a/app/src/main/java/be/digitalia/fosdem/activities/TrackScheduleEventActivity.java b/app/src/main/java/be/digitalia/fosdem/activities/TrackScheduleEventActivity.java index c5e6f12..d0b1a28 100644 --- a/app/src/main/java/be/digitalia/fosdem/activities/TrackScheduleEventActivity.java +++ b/app/src/main/java/be/digitalia/fosdem/activities/TrackScheduleEventActivity.java @@ -12,7 +12,6 @@ import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.view.MenuItem; -import android.view.View; import com.viewpagerindicator.PageIndicator; @@ -24,12 +23,12 @@ import be.digitalia.fosdem.model.Day; import be.digitalia.fosdem.model.Track; import be.digitalia.fosdem.utils.NfcUtils; import be.digitalia.fosdem.utils.NfcUtils.CreateNfcAppDataCallback; +import be.digitalia.fosdem.widgets.ContentLoadingProgressBar; /** * Event view of the track schedule; allows to slide between events of the same track using a ViewPager. - * + * * @author Christophe Beyls - * */ public class TrackScheduleEventActivity extends AppCompatActivity implements LoaderCallbacks, CreateNfcAppDataCallback { @@ -42,7 +41,7 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa private Day day; private Track track; private int initialPosition = -1; - private View progress; + private ContentLoadingProgressBar progress; private ViewPager pager; private PageIndicator pageIndicator; private TrackScheduleEventAdapter adapter; @@ -57,7 +56,7 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa day = extras.getParcelable(EXTRA_DAY); track = extras.getParcelable(EXTRA_TRACK); - progress = findViewById(R.id.progress); + progress = (ContentLoadingProgressBar) findViewById(R.id.progress); pager = (ViewPager) findViewById(R.id.pager); adapter = new TrackScheduleEventAdapter(getSupportFragmentManager()); pageIndicator = (PageIndicator) findViewById(R.id.indicator); @@ -81,7 +80,11 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa } private void setCustomProgressVisibility(boolean isVisible) { - progress.setVisibility(isVisible ? View.VISIBLE : View.GONE); + if (isVisible) { + progress.show(); + } else { + progress.hide(); + } } @Override @@ -99,9 +102,9 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case android.R.id.home: - finish(); - return true; + case android.R.id.home: + finish(); + return true; } return false; } diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/SmoothListFragment.java b/app/src/main/java/be/digitalia/fosdem/fragments/SmoothListFragment.java index 26f5683..c15feed 100644 --- a/app/src/main/java/be/digitalia/fosdem/fragments/SmoothListFragment.java +++ b/app/src/main/java/be/digitalia/fosdem/fragments/SmoothListFragment.java @@ -1,26 +1,177 @@ package be.digitalia.fosdem.fragments; -import android.os.Build; -import android.support.v4.app.ListFragment; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.FrameLayout; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import be.digitalia.fosdem.widgets.ContentLoadingProgressBar; /** - * ListFragment which disables the fade animation under certain conditions for more smoothness. + * Simpler ListFragment using ContentLoadingProgressBar. + * + * @author Christophe Beyls */ -public class SmoothListFragment extends ListFragment { +public class SmoothListFragment extends Fragment { + + private static final int DEFAULT_EMPTY_VIEW_PADDING_DIPS = 16; + + static class ViewHolder { + FrameLayout container; + View emptyView; + ListView listView; + ContentLoadingProgressBar progress; + } + + private final AdapterView.OnItemClickListener mOnClickListener = new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View v, int position, long id) { + onListItemClick((ListView) parent, v, position, id); + } + }; + + private ViewHolder mHolder; + private ListAdapter mAdapter; + private boolean mListShown; + + /** + * Override this method to provide a custom Empty View. + * The default one is a TextView with some padding. + */ + @NonNull + protected View onCreateEmptyView(LayoutInflater inflater, ViewGroup container, @Nullable Bundle savedInstanceState) { + TextView textView = new TextView(inflater.getContext()); + textView.setGravity(Gravity.CENTER); + int textPadding = (int) (getResources().getDisplayMetrics().density * DEFAULT_EMPTY_VIEW_PADDING_DIPS); + textView.setPadding(textPadding, textPadding, textPadding, textPadding); + return textView; + } @Override - public void setListShown(boolean shown) { - if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) && isResumed()) { - super.setListShown(shown); + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + final Context context = getActivity(); + + mHolder = new ViewHolder(); + + mHolder.container = new FrameLayout(context); + + mHolder.emptyView = onCreateEmptyView(inflater, mHolder.container, savedInstanceState); + mHolder.emptyView.setId(android.R.id.empty); + mHolder.container.addView(mHolder.emptyView, + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + mHolder.listView = new ListView(context); + mHolder.listView.setId(android.R.id.list); + mHolder.listView.setOnItemClickListener(mOnClickListener); + mHolder.container.addView(mHolder.listView, + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + mHolder.progress = new ContentLoadingProgressBar(context, null, android.R.attr.progressBarStyleLarge); + mHolder.progress.setId(android.R.id.progress); + mHolder.progress.hide(); + mHolder.container.addView(mHolder.progress, + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); + + mHolder.container.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + mListShown = true; + + if (mAdapter != null) { + setListAdapter(mAdapter); + mHolder.listView.setEmptyView(mHolder.emptyView); } else { - setListShownNoAnimation(shown); + setListShown(false); } + + return mHolder.container; } @Override public void onDestroyView() { // Ensure the ListView is properly unregistered as an observer of the adapter - getListView().setAdapter(null); + mHolder.listView.setAdapter(null); + mHolder = null; + mListShown = false; super.onDestroyView(); } -} + + /** + * This method will be called when an item in the list is selected. Subclasses should override. Subclasses can call + * getListView().getItemAtPosition(position) if they need to access the data associated with the selected item. + * + * @param l The ListView where the click happened + * @param v The view that was clicked within the ListView + * @param position The position of the view in the list + * @param id The row id of the item that was clicked + */ + public void onListItemClick(ListView l, View v, int position, long id) { + } + + public void setListAdapter(ListAdapter adapter) { + boolean hadNoAdapter = mAdapter == null; + mAdapter = adapter; + if (mHolder != null) { + mHolder.listView.setAdapter(adapter); + if (hadNoAdapter && !mListShown) { + // The list was hidden, and previously didn't have an + // adapter. It is now time to show it. + setListShown(true); + } + } + } + + /** + * Get the fragments's ListView widget. + */ + public ListView getListView() { + return mHolder.listView; + } + + /** + * The default content for a SwipeRefreshListFragment has a TextView that can be shown when the list is empty. + * Call this method to supply the text it should use. + */ + public void setEmptyText(CharSequence text) { + ((TextView) mHolder.emptyView).setText(text); + } + + /** + * Control whether the list is being displayed. You can make it not displayed if you are waiting for the initial data to show in it. During this time an + * indeterminate progress indicator will be shown instead. + * + * @param shown If true, the list view is shown; if false, the progress indicator. The initial value is true. + */ + public void setListShown(boolean shown) { + if (mListShown != shown) { + if (shown) { + mHolder.listView.setVisibility(View.VISIBLE); + mHolder.listView.setEmptyView(mHolder.emptyView); + mHolder.progress.hide(); + } else { + mHolder.listView.setEmptyView(null); + mHolder.listView.setVisibility(View.GONE); + mHolder.emptyView.setVisibility(View.GONE); + mHolder.progress.show(); + } + + mListShown = shown; + } + } + + /** + * Get the ListAdapter associated with this activity's ListView. + */ + public ListAdapter getListAdapter() { + return mAdapter; + } +} \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/TrackScheduleListFragment.java b/app/src/main/java/be/digitalia/fosdem/fragments/TrackScheduleListFragment.java index 0c9814d..2929622 100644 --- a/app/src/main/java/be/digitalia/fosdem/fragments/TrackScheduleListFragment.java +++ b/app/src/main/java/be/digitalia/fosdem/fragments/TrackScheduleListFragment.java @@ -189,7 +189,7 @@ public class TrackScheduleListFragment extends SmoothListFragment implements Han // Ensure the current selection is visible if (checkedPosition != ListView.INVALID_POSITION) { - setSelection(checkedPosition); + getListView().setSelection(checkedPosition); } // Notify the parent of the current selection to synchronize its state notifyEventSelected(checkedPosition); @@ -197,7 +197,7 @@ public class TrackScheduleListFragment extends SmoothListFragment implements Han } else if (!isListAlreadyShown) { int position = getDefaultPosition(); if (position != ListView.INVALID_POSITION) { - setSelection(position); + getListView().setSelection(position); } } isListAlreadyShown = true; diff --git a/app/src/main/java/be/digitalia/fosdem/widgets/ContentLoadingProgressBar.java b/app/src/main/java/be/digitalia/fosdem/widgets/ContentLoadingProgressBar.java new file mode 100644 index 0000000..bcec3b4 --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/widgets/ContentLoadingProgressBar.java @@ -0,0 +1,121 @@ +package be.digitalia.fosdem.widgets; + +import android.content.Context; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ProgressBar; + +/** + * ContentLoadingProgressBar implements a ProgressBar that waits a minimum time to be + * dismissed before showing. Once visible, the progress bar will be visible for + * a minimum amount of time to avoid "flashes" in the UI when an event could take + * a largely variable time to complete (from none, to a user perceivable amount). + *

+ * This version is similar to the support library version but implemented "the right way". + * + * @author Christophe Beyls + */ +public class ContentLoadingProgressBar extends ProgressBar { + private static final long MIN_SHOW_TIME = 500L; // ms + private static final long MIN_DELAY = 500L; // ms + + private boolean mIsAttachedToWindow = false; + private boolean mIsShown; + long mStartTime = -1L; + + private final Runnable mDelayedHide = new Runnable() { + + @Override + public void run() { + setVisibility(View.GONE); + mStartTime = -1L; + } + }; + + private final Runnable mDelayedShow = new Runnable() { + + @Override + public void run() { + mStartTime = SystemClock.uptimeMillis(); + setVisibility(View.VISIBLE); + } + }; + + public ContentLoadingProgressBar(Context context) { + this(context, null, 0); + } + + public ContentLoadingProgressBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ContentLoadingProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mIsShown = getVisibility() == View.VISIBLE; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mIsAttachedToWindow = true; + if (mIsShown && (getVisibility() != View.VISIBLE)) { + postDelayed(mDelayedShow, MIN_DELAY); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mIsAttachedToWindow = false; + removeCallbacks(mDelayedHide); + removeCallbacks(mDelayedShow); + if (!mIsShown && mStartTime != -1L) { + setVisibility(View.GONE); + } + mStartTime = -1L; + } + + /** + * Hide the progress view if it is visible. The progress view will not be + * hidden until it has been shown for at least a minimum show time. If the + * progress view was not yet visible, cancels showing the progress view. + */ + public void hide() { + if (mIsShown) { + mIsShown = false; + if (mIsAttachedToWindow) { + removeCallbacks(mDelayedShow); + } + long diff = SystemClock.uptimeMillis() - mStartTime; + if (mStartTime == -1L || diff >= MIN_SHOW_TIME) { + // The progress spinner has been shown long enough + // OR was not shown yet. If it wasn't shown yet, + // it will just never be shown. + setVisibility(View.GONE); + mStartTime = -1L; + } else { + // The progress spinner is shown, but not long enough, + // so put a delayed message in to hide it when its been + // shown long enough. + postDelayed(mDelayedHide, MIN_SHOW_TIME - diff); + } + } + } + + /** + * Show the progress view after waiting for a minimum delay. If + * during that time, hide() is called, the view is never made visible. + */ + public void show() { + if (!mIsShown) { + mIsShown = true; + if (mIsAttachedToWindow) { + removeCallbacks(mDelayedHide); + if (mStartTime == -1L) { + postDelayed(mDelayedShow, MIN_DELAY); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/track_schedule_event.xml b/app/src/main/res/layout/track_schedule_event.xml index 31d9716..08d7ac6 100644 --- a/app/src/main/res/layout/track_schedule_event.xml +++ b/app/src/main/res/layout/track_schedule_event.xml @@ -3,7 +3,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> -