Improvements to ListFragment.

Now deals correctly with a content view containing just a list,
and adds a lot more built-in functionality: ability to show
custom text for an empty list, and indeterminant progress while
populating the list.

In addition, reworks transaction committing to be more aggressive
about committing the transactions as the containing activity
moves between its states (rather than waiting for the activity's
handler to process the transaction message whenever that may
finally happen).  And fixed a bug with saving/restoring state of
transaction replace operations.

Change-Id: I9617a0c4f248b50a61b319910323639b6de24f73
This commit is contained in:
Dianne Hackborn
2010-06-25 15:52:59 -07:00
parent 623e78b792
commit 445646c521
8 changed files with 288 additions and 19 deletions

View File

@@ -28030,6 +28030,19 @@
<parameter name="id" type="long">
</parameter>
</method>
<method name="setEmptyText"
return="void"
abstract="false"
native="false"
synchronized="false"
static="false"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="text" type="java.lang.CharSequence">
</parameter>
</method>
<method name="setListAdapter"
return="void"
abstract="false"
@@ -28043,6 +28056,21 @@
<parameter name="adapter" type="android.widget.ListAdapter">
</parameter>
</method>
<method name="setListShown"
return="void"
abstract="false"
native="false"
synchronized="false"
static="false"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="shown" type="boolean">
</parameter>
<parameter name="animate" type="boolean">
</parameter>
</method>
<method name="setSelection"
return="void"
abstract="false"
@@ -28152,7 +28180,7 @@
static="false"
final="false"
deprecated="not deprecated"
visibility="protected"
visibility="public"
>
<parameter name="id" type="int">
</parameter>
@@ -223896,7 +223924,7 @@
deprecated="not deprecated"
visibility="public"
>
<parameter name="t" type="T">
<parameter name="arg0" type="T">
</parameter>
</method>
</interface>
@@ -247337,7 +247365,7 @@
<method name="availableProcessors"
return="int"
abstract="false"
native="false"
native="true"
synchronized="false"
static="false"
final="false"

View File

@@ -4036,6 +4036,7 @@ public class Activity extends ContextThemeWrapper
final void performStart() {
mCalled = false;
mFragments.execPendingActions();
mInstrumentation.callActivityOnStart(this);
if (!mCalled) {
throw new SuperNotCalledException(
@@ -4074,6 +4075,8 @@ public class Activity extends ContextThemeWrapper
final void performResume() {
performRestart();
mFragments.execPendingActions();
mLastNonConfigurationInstances = null;
// First call onResume() -before- setting mResumed, so we don't
@@ -4091,6 +4094,7 @@ public class Activity extends ContextThemeWrapper
mCalled = false;
mFragments.dispatchResume();
mFragments.execPendingActions();
onPostResume();
if (!mCalled) {

View File

@@ -18,6 +18,7 @@ package android.app;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import java.util.ArrayList;
@@ -28,14 +29,30 @@ final class BackStackState implements Parcelable {
final String mName;
public BackStackState(FragmentManager fm, BackStackEntry bse) {
mOps = new int[bse.mNumOp*4];
int numRemoved = 0;
BackStackEntry.Op op = bse.mHead;
while (op != null) {
if (op.removed != null) numRemoved += op.removed.size();
op = op.next;
}
mOps = new int[bse.mNumOp*5 + numRemoved];
op = bse.mHead;
int pos = 0;
while (op != null) {
mOps[pos++] = op.cmd;
mOps[pos++] = op.fragment.mIndex;
mOps[pos++] = op.enterAnim;
mOps[pos++] = op.exitAnim;
if (op.removed != null) {
final int N = op.removed.size();
mOps[pos++] = N;
for (int i=0; i<N; i++) {
mOps[pos++] = op.removed.get(i).mIndex;
}
} else {
mOps[pos++] = 0;
}
op = op.next;
}
mTransition = bse.mTransition;
@@ -61,6 +78,13 @@ final class BackStackState implements Parcelable {
op.fragment = f;
op.enterAnim = mOps[pos++];
op.exitAnim = mOps[pos++];
final int N = mOps[pos++];
if (N > 0) {
op.removed = new ArrayList<Fragment>(N);
for (int i=0; i<N; i++) {
op.removed.add(fm.mActive.get(mOps[pos++]));
}
}
bse.addOp(op);
}
bse.mTransition = mTransition;
@@ -96,6 +120,8 @@ final class BackStackState implements Parcelable {
* @hide Entry of an operation on the fragment back stack.
*/
final class BackStackEntry implements FragmentTransaction, Runnable {
static final String TAG = "BackStackEntry";
final FragmentManager mManager;
static final int OP_NULL = 0;
@@ -265,11 +291,14 @@ final class BackStackEntry implements FragmentTransaction, Runnable {
public void commit() {
if (mCommitted) throw new IllegalStateException("commit already called");
if (FragmentManager.DEBUG) Log.v(TAG, "Commit: " + this);
mCommitted = true;
mManager.mActivity.mHandler.post(this);
mManager.enqueueAction(this);
}
public void run() {
if (FragmentManager.DEBUG) Log.v(TAG, "Run: " + this);
Op op = mHead;
while (op != null) {
switch (op.cmd) {
@@ -286,6 +315,8 @@ final class BackStackEntry implements FragmentTransaction, Runnable {
if (mManager.mAdded != null) {
for (int i=0; i<mManager.mAdded.size(); i++) {
Fragment old = mManager.mAdded.get(i);
if (FragmentManager.DEBUG) Log.v(TAG,
"OP_REPLACE: adding=" + f + " old=" + old);
if (old.mContainerId == f.mContainerId) {
if (op.removed == null) {
op.removed = new ArrayList<Fragment>();
@@ -350,6 +381,8 @@ final class BackStackEntry implements FragmentTransaction, Runnable {
}
public void popFromBackStack() {
if (FragmentManager.DEBUG) Log.v(TAG, "popFromBackStack: " + this);
Op op = mTail;
while (op != null) {
switch (op.cmd) {

View File

@@ -77,6 +77,10 @@ public class FragmentManager {
static final boolean DEBUG = true;
static final String TAG = "FragmentManager";
ArrayList<Runnable> mPendingActions;
Runnable[] mTmpActions;
boolean mExecutingActions;
ArrayList<Fragment> mActive;
ArrayList<Fragment> mAdded;
ArrayList<Integer> mAvailIndices;
@@ -91,6 +95,13 @@ public class FragmentManager {
Bundle mStateBundle = null;
SparseArray<Parcelable> mStateArray = null;
Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}
};
Animation loadAnimation(Fragment fragment, int transit, boolean enter,
int transitionStyle) {
Animation animObj = fragment.onCreateAnimation(transitionStyle, enter,
@@ -486,6 +497,52 @@ public class FragmentManager {
return null;
}
public void enqueueAction(Runnable action) {
synchronized (this) {
if (mPendingActions == null) {
mPendingActions = new ArrayList<Runnable>();
}
mPendingActions.add(action);
if (mPendingActions.size() == 1) {
mActivity.mHandler.removeCallbacks(mExecCommit);
mActivity.mHandler.post(mExecCommit);
}
}
}
/**
* Only call from main thread!
*/
public void execPendingActions() {
if (mExecutingActions) {
throw new IllegalStateException("Recursive entry to execPendingActions");
}
while (true) {
int numActions;
synchronized (this) {
if (mPendingActions == null || mPendingActions.size() == 0) {
return;
}
numActions = mPendingActions.size();
if (mTmpActions == null || mTmpActions.length < numActions) {
mTmpActions = new Runnable[numActions];
}
mPendingActions.toArray(mTmpActions);
mPendingActions.clear();
mActivity.mHandler.removeCallbacks(mExecCommit);
}
mExecutingActions = true;
for (int i=0; i<numActions; i++) {
mTmpActions[i].run();
}
mExecutingActions = false;
}
}
public void addBackStackState(BackStackEntry state) {
if (mBackStack == null) {
mBackStack = new ArrayList<BackStackEntry>();
@@ -503,8 +560,9 @@ public class FragmentManager {
return false;
}
final BackStackEntry bss = mBackStack.remove(last);
handler.post(new Runnable() {
enqueueAction(new Runnable() {
public void run() {
if (DEBUG) Log.v(TAG, "Popping back stack state: " + bss);
bss.popFromBackStack();
moveToState(mCurState, reverseTransit(bss.getTransition()),
bss.getTransitionStyle(), true);
@@ -526,9 +584,10 @@ public class FragmentManager {
for (int i=mBackStack.size()-1; i>index; i--) {
states.add(mBackStack.remove(i));
}
handler.post(new Runnable() {
enqueueAction(new Runnable() {
public void run() {
for (int i=0; i<states.size(); i++) {
if (DEBUG) Log.v(TAG, "Popping back stack state: " + states.get(i));
states.get(i).popFromBackStack();
}
moveToState(mCurState, true);

View File

@@ -21,9 +21,11 @@ import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
/**
* An fragment that displays a list of items by binding to a data source such as
@@ -154,17 +156,28 @@ public class ListFragment extends Fragment {
ListAdapter mAdapter;
ListView mList;
View mEmptyView;
TextView mStandardEmptyView;
View mProgressContainer;
View mListContainer;
boolean mSetEmptyView;
boolean mListShown;
public ListFragment() {
}
/**
* Provide default implementation to return a simple list view.
* Provide default implementation to return a simple list view. Subclasses
* can override to replace with their own layout. If doing so, the
* returned view hierarchy <em>must</em> have a ListView whose id
* is {@link android.R.id.list android.R.id.list} and can optionally
* have a sibling view id {@link android.R.id.empty android.R.id.empty}
* that is to be shown when the list is empty.
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(com.android.internal.R.layout.list_content,
return inflater.inflate(com.android.internal.R.layout.list_content_rich,
container, false);
}
@@ -246,6 +259,62 @@ public class ListFragment extends Fragment {
return mList;
}
/**
* The default content for a ListFragment has a TextView that can
* be shown when the list is empty. If you would like to have it
* shown, call this method to supply the text it should use.
*/
public void setEmptyText(CharSequence text) {
ensureList();
if (mStandardEmptyView == null) {
throw new IllegalStateException("Can't be used with a custom content view");
}
if (!mSetEmptyView) {
mSetEmptyView = true;
mList.setEmptyView(mStandardEmptyView);
}
}
/**
* 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 indeterminant 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.
* @param animate If true, an animation will be used to transition to the
* new state.
*/
public void setListShown(boolean shown, boolean animate) {
ensureList();
if (mProgressContainer == null) {
throw new IllegalStateException("Can't be used with a custom content view");
}
if (mListShown == shown) {
return;
}
mListShown = shown;
if (shown) {
if (animate) {
mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
getActivity(), android.R.anim.fade_out));
mListContainer.startAnimation(AnimationUtils.loadAnimation(
getActivity(), android.R.anim.fade_in));
}
mProgressContainer.setVisibility(View.GONE);
mListContainer.setVisibility(View.VISIBLE);
} else {
if (animate) {
mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
getActivity(), android.R.anim.fade_in));
mListContainer.startAnimation(AnimationUtils.loadAnimation(
getActivity(), android.R.anim.fade_out));
}
mProgressContainer.setVisibility(View.VISIBLE);
mListContainer.setVisibility(View.GONE);
}
}
/**
* Get the ListAdapter associated with this activity's ListView.
*/
@@ -261,16 +330,33 @@ public class ListFragment extends Fragment {
if (root == null) {
throw new IllegalStateException("Content view not yet created");
}
View emptyView = root.findViewById(com.android.internal.R.id.empty);
mList = (ListView)root.findViewById(com.android.internal.R.id.list);
if (mList == null) {
throw new RuntimeException(
"Your content must have a ListView whose id attribute is " +
"'android.R.id.list'");
}
if (emptyView != null) {
mList.setEmptyView(emptyView);
if (root instanceof ListView) {
mList = (ListView)root;
} else {
mStandardEmptyView = (TextView)root.findViewById(
com.android.internal.R.id.internalEmpty);
if (mStandardEmptyView == null) {
mEmptyView = root.findViewById(android.R.id.empty);
}
mProgressContainer = root.findViewById(com.android.internal.R.id.progressContainer);
mListContainer = root.findViewById(com.android.internal.R.id.listContainer);
View rawListView = root.findViewById(android.R.id.list);
if (!(rawListView instanceof ListView)) {
throw new RuntimeException(
"Content has view with id attribute 'android.R.id.list' "
+ "that is not a ListView class");
}
mList = (ListView)rawListView;
if (mList == null) {
throw new RuntimeException(
"Your content must have a ListView whose id attribute is " +
"'android.R.id.list'");
}
if (mEmptyView != null) {
mList.setEmptyView(mEmptyView);
}
}
mListShown = true;
mList.setOnItemClickListener(mOnClickListener);
if (mAdapter != null) {
setListAdapter(mAdapter);

View File

@@ -44,7 +44,7 @@ public abstract class LoaderManagingFragment<D> extends Fragment
* when the new loader completes it's work. The callback will be delivered before the old loader
* is destroyed.
*/
protected Loader<D> startLoading(int id, Bundle args) {
public Loader<D> startLoading(int id, Bundle args) {
LoaderInfo<D> info = mLoaders.get(id);
if (info != null) {
// Keep track of the previous instance of this loader so we can destroy

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/* Copyright 2010, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout android:id="@+id/progressContainer"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:gravity="center">
<ProgressBar style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="@string/loading"
android:paddingTop="4dip"
android:singleLine="true" />
</LinearLayout>
<FrameLayout android:id="@+id/listContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawSelectorOnTop="false" />
<TextView android:id="@+android:id/internalEmpty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textAppearance="?android:attr/textAppearanceLarge" />
</FrameLayout>
</FrameLayout>

View File

@@ -1919,6 +1919,9 @@
combined with setIcon(android.R.drawable.ic_dialog_alert) -->
<string name="dialog_alert_title">Attention</string>
<!-- Text shown by list fragment when waiting for data to display. -->
<string name="loading">Loading...</string>
<!-- Default text for a button that can be toggled on and off. -->
<string name="capital_on">ON</string>
<!-- Default text for a button that can be toggled on and off. -->