blob: b61673cd72971a73c3c96d5714ed7cd5efddda7b [file] [log] [blame]
/*
* Copyright (C) 2008 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.
*/
package android.widget.cts.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import android.app.Activity;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
/**
* Utility base class for creating various List scenarios. Configurable by the number
* of items, how tall each item should be (in relation to the screen height), and
* what item should start with selection.
*/
public abstract class ListScenario extends Activity {
private ListView mListView;
private TextView mHeaderTextView;
private int mNumItems;
protected boolean mItemsFocusable;
private int mStartingSelectionPosition;
private double mItemScreenSizeFactor;
private Map<Integer, Double> mOverrideItemScreenSizeFactors = new HashMap<>();
private int mScreenHeight;
// whether to include a text view above the list
private boolean mIncludeHeader;
// separators
private Set<Integer> mUnselectableItems = new HashSet<Integer>();
private boolean mStackFromBottom;
private int mClickedPosition = -1;
private int mLongClickedPosition = -1;
private int mConvertMisses = 0;
private int mHeaderViewCount;
private boolean mHeadersFocusable;
private int mFooterViewCount;
private LinearLayout mLinearLayout;
public ListView getListView() {
return mListView;
}
protected int getScreenHeight() {
return mScreenHeight;
}
/**
* Return whether the item at position is selectable (i.e is a separator).
* (external users can access this info using the adapter)
*/
private boolean isItemAtPositionSelectable(int position) {
return !mUnselectableItems.contains(position);
}
/**
* Better way to pass in optional params than a honkin' paramater list :)
*/
public static class Params {
private int mNumItems = 4;
private boolean mItemsFocusable = false;
private int mStartingSelectionPosition = 0;
private double mItemScreenSizeFactor = 1 / 5;
private Double mFadingEdgeScreenSizeFactor = null;
private Map<Integer, Double> mOverrideItemScreenSizeFactors = new HashMap<>();
// separators
private List<Integer> mUnselectableItems = new ArrayList<Integer>(8);
// whether to include a text view above the list
private boolean mIncludeHeader = false;
private boolean mStackFromBottom = false;
public boolean mMustFillScreen = true;
private int mHeaderViewCount;
private boolean mHeaderFocusable = false;
private int mFooterViewCount;
private boolean mConnectAdapter = true;
/**
* Set the number of items in the list.
*/
public Params setNumItems(int numItems) {
mNumItems = numItems;
return this;
}
/**
* Set whether the items are focusable.
*/
public Params setItemsFocusable(boolean itemsFocusable) {
mItemsFocusable = itemsFocusable;
return this;
}
/**
* Set the position that starts selected.
*
* @param startingSelectionPosition The selected position within the adapter's data set.
* Pass -1 if you do not want to force a selection.
* @return
*/
public Params setStartingSelectionPosition(int startingSelectionPosition) {
mStartingSelectionPosition = startingSelectionPosition;
return this;
}
/**
* Set the factor that determines how tall each item is in relation to the
* screen height.
*/
public Params setItemScreenSizeFactor(double itemScreenSizeFactor) {
mItemScreenSizeFactor = itemScreenSizeFactor;
return this;
}
/**
* Override the item screen size factor for a particular item. Useful for
* creating lists with non-uniform item height.
* @param position The position in the list.
* @param itemScreenSizeFactor The screen size factor to use for the height.
*/
public Params setPositionScreenSizeFactorOverride(
int position, double itemScreenSizeFactor) {
mOverrideItemScreenSizeFactors.put(position, itemScreenSizeFactor);
return this;
}
/**
* Set a position as unselectable (a.k.a a separator)
* @param position
* @return
*/
public Params setPositionUnselectable(int position) {
mUnselectableItems.add(position);
return this;
}
/**
* Set positions as unselectable (a.k.a a separator)
*/
public Params setPositionsUnselectable(int ...positions) {
for (int pos : positions) {
setPositionUnselectable(pos);
}
return this;
}
/**
* Include a header text view above the list.
* @param includeHeader
* @return
*/
public Params includeHeaderAboveList(boolean includeHeader) {
mIncludeHeader = includeHeader;
return this;
}
/**
* Sets the stacking direction
* @param stackFromBottom
* @return
*/
public Params setStackFromBottom(boolean stackFromBottom) {
mStackFromBottom = stackFromBottom;
return this;
}
/**
* Sets whether the sum of the height of the list items must be at least the
* height of the list view.
*/
public Params setMustFillScreen(boolean fillScreen) {
mMustFillScreen = fillScreen;
return this;
}
/**
* Set the factor for the fading edge length.
*/
public Params setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor) {
mFadingEdgeScreenSizeFactor = fadingEdgeScreenSizeFactor;
return this;
}
/**
* Set the number of header views to appear within the list
*/
public Params setHeaderViewCount(int headerViewCount) {
mHeaderViewCount = headerViewCount;
return this;
}
/**
* Set whether the headers should be focusable.
* @param headerFocusable Whether the headers should be focusable (i.e
* created as edit texts rather than text views).
*/
public Params setHeaderFocusable(boolean headerFocusable) {
mHeaderFocusable = headerFocusable;
return this;
}
/**
* Set the number of footer views to appear within the list
*/
public Params setFooterViewCount(int footerViewCount) {
mFooterViewCount = footerViewCount;
return this;
}
/**
* Sets whether the {@link ListScenario} will automatically set the
* adapter on the list view. If this is false, the client MUST set it
* manually (this is useful when adding headers to the list view, which
* must be done before the adapter is set).
*/
public Params setConnectAdapter(boolean connectAdapter) {
mConnectAdapter = connectAdapter;
return this;
}
}
/**
* How each scenario customizes its behavior.
* @param params
*/
protected abstract void init(Params params);
/**
* Override this if you want to know when something has been selected (perhaps
* more importantly, that {@link android.widget.AdapterView.OnItemSelectedListener} has
* been triggered).
*/
protected void positionSelected(int positon) {
}
/**
* Override this if you want to know that nothing is selected.
*/
protected void nothingSelected() {
}
/**
* Override this if you want to know when something has been clicked (perhaps
* more importantly, that {@link android.widget.AdapterView.OnItemClickListener} has
* been triggered).
*/
protected void positionClicked(int position) {
setClickedPosition(position);
}
/**
* Override this if you want to know when something has been long clicked (perhaps
* more importantly, that {@link android.widget.AdapterView.OnItemLongClickListener} has
* been triggered).
*/
protected void positionLongClicked(int position) {
setLongClickedPosition(position);
}
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
// for test stability, turn off title bar
requestWindowFeature(Window.FEATURE_NO_TITLE);
mScreenHeight = getWindowManager().getDefaultDisplay().getHeight();
final Params params = createParams();
init(params);
readAndValidateParams(params);
mListView = createListView();
mListView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
mListView.setDrawSelectorOnTop(false);
for (int i=0; i<mHeaderViewCount; i++) {
TextView header = mHeadersFocusable ?
new EditText(this) :
new TextView(this);
header.setText("Header: " + i);
mListView.addHeaderView(header);
}
for (int i=0; i<mFooterViewCount; i++) {
TextView header = new TextView(this);
header.setText("Footer: " + i);
mListView.addFooterView(header);
}
if (params.mConnectAdapter) {
setAdapter(mListView);
}
mListView.setItemsCanFocus(mItemsFocusable);
if (mStartingSelectionPosition >= 0) {
mListView.setSelection(mStartingSelectionPosition);
}
mListView.setPadding(0, 0, 0, 0);
mListView.setStackFromBottom(mStackFromBottom);
mListView.setDivider(null);
mListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
public void onItemSelected(AdapterView parent, View v, int position, long id) {
positionSelected(position);
}
public void onNothingSelected(AdapterView parent) {
nothingSelected();
}
});
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView parent, View v, int position, long id) {
positionClicked(position);
}
});
// set the fading edge length porportionally to the screen
// height for test stability
if (params.mFadingEdgeScreenSizeFactor != null) {
mListView.setFadingEdgeLength((int) (params.mFadingEdgeScreenSizeFactor * mScreenHeight));
} else {
mListView.setFadingEdgeLength((int) ((64.0 / 480) * mScreenHeight));
}
if (mIncludeHeader) {
mLinearLayout = new LinearLayout(this);
mHeaderTextView = new TextView(this);
mHeaderTextView.setText("hi");
mHeaderTextView.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
mLinearLayout.addView(mHeaderTextView);
mLinearLayout.setOrientation(LinearLayout.VERTICAL);
mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
mListView.setLayoutParams((new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1f)));
mLinearLayout.addView(mListView);
setContentView(mLinearLayout);
} else {
mLinearLayout = new LinearLayout(this);
mLinearLayout.setOrientation(LinearLayout.VERTICAL);
mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
mListView.setLayoutParams((new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1f)));
mLinearLayout.addView(mListView);
setContentView(mLinearLayout);
}
}
/**
* Returns the LinearLayout containing the ListView in this scenario.
*
* @return The LinearLayout in which the ListView is held.
*/
protected LinearLayout getListViewContainer() {
return mLinearLayout;
}
/**
* Attaches a long press listener. You can find out which views were clicked by calling
* {@link #getLongClickedPosition()}.
*/
public void enableLongPress() {
mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
public boolean onItemLongClick(AdapterView parent, View v, int position, long id) {
positionLongClicked(position);
return true;
}
});
}
/**
* @return The newly created ListView widget.
*/
protected ListView createListView() {
return new ListView(this);
}
/**
* @return The newly created Params object.
*/
protected Params createParams() {
return new Params();
}
/**
* Sets an adapter on a ListView.
*
* @param listView The ListView to set the adapter on.
*/
protected void setAdapter(ListView listView) {
listView.setAdapter(new MyAdapter());
}
/**
* Read in and validate all of the params passed in by the scenario.
* @param params
*/
protected void readAndValidateParams(Params params) {
if (params.mMustFillScreen ) {
double totalFactor = 0.0;
for (int i = 0; i < params.mNumItems; i++) {
if (params.mOverrideItemScreenSizeFactors.containsKey(i)) {
totalFactor += params.mOverrideItemScreenSizeFactors.get(i);
} else {
totalFactor += params.mItemScreenSizeFactor;
}
}
if (totalFactor < 1.0) {
throw new IllegalArgumentException("list items must combine to be at least " +
"the height of the screen. this is not the case with " + params.mNumItems
+ " items and " + params.mItemScreenSizeFactor + " screen factor and " +
"screen height of " + mScreenHeight);
}
}
mNumItems = params.mNumItems;
mItemsFocusable = params.mItemsFocusable;
mStartingSelectionPosition = params.mStartingSelectionPosition;
mItemScreenSizeFactor = params.mItemScreenSizeFactor;
mOverrideItemScreenSizeFactors.putAll(params.mOverrideItemScreenSizeFactors);
mUnselectableItems.addAll(params.mUnselectableItems);
mIncludeHeader = params.mIncludeHeader;
mStackFromBottom = params.mStackFromBottom;
mHeaderViewCount = params.mHeaderViewCount;
mHeadersFocusable = params.mHeaderFocusable;
mFooterViewCount = params.mFooterViewCount;
}
public final String getValueAtPosition(int position) {
return isItemAtPositionSelectable(position)
?
"position " + position:
"------- " + position;
}
/**
* @return The height that will be set for a particular position.
*/
public int getHeightForPosition(int position) {
int desiredHeight = (int) (mScreenHeight * mItemScreenSizeFactor);
if (mOverrideItemScreenSizeFactors.containsKey(position)) {
desiredHeight = (int) (mScreenHeight * mOverrideItemScreenSizeFactors.get(position));
}
return desiredHeight;
}
/**
* @return The contents of the header above the list.
* @throws IllegalArgumentException if there is no header.
*/
public final String getHeaderValue() {
if (!mIncludeHeader) {
throw new IllegalArgumentException("no header above list");
}
return mHeaderTextView.getText().toString();
}
/**
* @param value What to put in the header text view
* @throws IllegalArgumentException if there is no header.
*/
protected final void setHeaderValue(String value) {
if (!mIncludeHeader) {
throw new IllegalArgumentException("no header above list");
}
mHeaderTextView.setText(value);
}
/**
* Create a view for a list item. Override this to create a custom view beyond
* the simple focusable / unfocusable text view.
* @param position The position.
* @param parent The parent
* @param desiredHeight The height the view should be to respect the desired item
* to screen height ratio.
* @return a view for the list.
*/
protected View createView(int position, ViewGroup parent, int desiredHeight) {
return ListItemFactory.text(position, parent.getContext(), getValueAtPosition(position),
desiredHeight);
}
/**
* Convert a non-null view.
*/
public View convertView(int position, View convertView, ViewGroup parent) {
return ListItemFactory.convertText(convertView, getValueAtPosition(position), position);
}
public void setClickedPosition(int clickedPosition) {
mClickedPosition = clickedPosition;
}
public int getClickedPosition() {
return mClickedPosition;
}
public void setLongClickedPosition(int longClickedPosition) {
mLongClickedPosition = longClickedPosition;
}
public int getLongClickedPosition() {
return mLongClickedPosition;
}
/**
* Have a child of the list view call {@link View#requestRectangleOnScreen(android.graphics.Rect)}.
* @param childIndex The index into the viewgroup children (i.e the children that are
* currently visible).
* @param rect The rectangle, in the child's coordinates.
*/
public void requestRectangleOnScreen(int childIndex, final Rect rect) {
final View child = getListView().getChildAt(childIndex);
child.post(new Runnable() {
public void run() {
child.requestRectangleOnScreen(rect);
}
});
}
/**
* Return an item type for the specified position in the adapter. Override if your
* adapter creates more than one type.
*/
public int getItemViewType(int position) {
return 0;
}
/**
* Return an the number of types created by the adapter. Override if your
* adapter creates more than one type.
*/
public int getViewTypeCount() {
return 1;
}
/**
* @return The number of times convertView failed
*/
public int getConvertMisses() {
return mConvertMisses;
}
private class MyAdapter extends BaseAdapter {
public int getCount() {
return mNumItems;
}
public Object getItem(int position) {
return getValueAtPosition(position);
}
public long getItemId(int position) {
return position;
}
@Override
public boolean areAllItemsEnabled() {
return mUnselectableItems.isEmpty();
}
@Override
public boolean isEnabled(int position) {
return isItemAtPositionSelectable(position);
}
public View getView(int position, View convertView, ViewGroup parent) {
View result = null;
if (position >= mNumItems || position < 0) {
throw new IllegalStateException("position out of range for adapter!");
}
if (convertView != null) {
result = convertView(position, convertView, parent);
if (result == null) {
mConvertMisses++;
}
}
if (result == null) {
int desiredHeight = getHeightForPosition(position);
result = createView(position, parent, desiredHeight);
}
return result;
}
@Override
public int getItemViewType(int position) {
return ListScenario.this.getItemViewType(position);
}
@Override
public int getViewTypeCount() {
return ListScenario.this.getViewTypeCount();
}
}
}