blob: 486e877318dd12d5c20b8881e0913809f6aee219 [file] [log] [blame]
/*
* Copyright (C) 2015 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.support.v17.leanback.widget.picker;
import android.content.Context;
import android.graphics.Rect;
import android.support.v17.leanback.R;
import android.support.v17.leanback.widget.OnChildViewHolderSelectedListener;
import android.support.v17.leanback.widget.VerticalGridView;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Picker is a widget showing multiple customized {@link PickerColumn}s. The PickerColumns are
* initialized in {@link #setColumns(List)}. Call {@link #setColumnAt(int, PickerColumn)} if the
* column value range or labels change. Call {@link #setColumnValue(int, int, boolean)} to update
* the current value of PickerColumn.
* <p>
* Picker has two states and will change height:
* <li>{@link #isActivated()} is true: Picker shows typically three items vertically (see
* {@link #getActivatedVisibleItemCount()}}. Columns other than {@link #getSelectedColumn()} still
* shows one item if the Picker is focused. On a touch screen device, the Picker will not get focus
* so it always show three items on all columns. On a non-touch device (a TV), the Picker will show
* three items only on currently activated column. If the Picker has focus, it will intercept DPAD
* directions and select activated column.
* <li>{@link #isActivated()} is false: Picker shows one item vertically (see
* {@link #getVisibleItemCount()}) on all columns. The size of Picker shrinks.
*/
public class Picker extends FrameLayout {
public interface PickerValueListener {
public void onValueChanged(Picker picker, int column);
}
private ViewGroup mRootView;
private ViewGroup mPickerView;
final List<VerticalGridView> mColumnViews = new ArrayList<VerticalGridView>();
ArrayList<PickerColumn> mColumns;
private float mUnfocusedAlpha;
private float mFocusedAlpha;
private float mVisibleColumnAlpha;
private float mInvisibleColumnAlpha;
private int mAlphaAnimDuration;
private Interpolator mDecelerateInterpolator;
private Interpolator mAccelerateInterpolator;
private ArrayList<PickerValueListener> mListeners;
private float mVisibleItemsActivated = 3;
private float mVisibleItems = 1;
private int mSelectedColumn = 0;
private List<CharSequence> mSeparators = new ArrayList<>();
private int mPickerItemLayoutId = R.layout.lb_picker_item;
private int mPickerItemTextViewId = 0;
/**
* Gets separator string between columns.
*
* @return The separator that will be populated between all the Picker columns.
* @deprecated Use {@link #getSeparators()}
*/
public final CharSequence getSeparator() {
return mSeparators.get(0);
}
/**
* Sets separator String between Picker columns.
*
* @param separator Separator String between Picker columns.
*/
public final void setSeparator(CharSequence separator) {
setSeparators(Arrays.asList(separator));
}
/**
* Returns the list of separators that will be populated between the picker column fields.
*
* @return The list of separators populated between the picker column fields.
*/
public final List<CharSequence> getSeparators() {
return mSeparators;
}
/**
* Sets the list of separators that will be populated between the Picker columns. The
* number of the separators should be either 1 indicating the same separator used between all
* the columns fields (and nothing will be placed before the first and after the last column),
* or must be one unit larger than the number of columns passed to {@link #setColumns(List)}.
* In the latter case, the list of separators corresponds to the positions before the first
* column all the way to the position after the last column.
* An empty string for a given position indicates no separators needs to be placed for that
* position, otherwise a TextView with the given String will be created and placed there.
*
* @param separators The list of separators to be populated between the Picker columns.
*/
public final void setSeparators(List<CharSequence> separators) {
mSeparators.clear();
mSeparators.addAll(separators);
}
/**
* Classes extending {@link Picker} can choose to override this method to
* supply the {@link Picker}'s item's layout id
*/
public final int getPickerItemLayoutId() {
return mPickerItemLayoutId;
}
/**
* Returns the {@link Picker}'s item's {@link TextView}'s id from within the
* layout provided by {@link Picker#getPickerItemLayoutId()} or 0 if the
* layout provided by {@link Picker#getPickerItemLayoutId()} is a {link
* TextView}.
*/
public final int getPickerItemTextViewId() {
return mPickerItemTextViewId;
}
/**
* Sets the {@link Picker}'s item's {@link TextView}'s id from within the
* layout provided by {@link Picker#getPickerItemLayoutId()} or 0 if the
* layout provided by {@link Picker#getPickerItemLayoutId()} is a {link
* TextView}.
*
* @param textViewId View id of TextView inside a Picker item, or 0 if the Picker item is a
* TextView.
*/
public final void setPickerItemTextViewId(int textViewId) {
mPickerItemTextViewId = textViewId;
}
/**
* Creates a Picker widget.
*/
public Picker(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// Make it enabled and clickable to receive Click event.
setEnabled(true);
setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mFocusedAlpha = 1f; //getFloat(R.dimen.list_item_selected_title_text_alpha);
mUnfocusedAlpha = 1f; //getFloat(R.dimen.list_item_unselected_text_alpha);
mVisibleColumnAlpha = 0.5f; //getFloat(R.dimen.picker_item_visible_column_item_alpha);
mInvisibleColumnAlpha = 0f; //getFloat(R.dimen.picker_item_invisible_column_item_alpha);
mAlphaAnimDuration =
200; // mContext.getResources().getInteger(R.integer.dialog_animation_duration);
mDecelerateInterpolator = new DecelerateInterpolator(2.5F);
mAccelerateInterpolator = new AccelerateInterpolator(2.5F);
LayoutInflater inflater = LayoutInflater.from(getContext());
mRootView = (ViewGroup) inflater.inflate(R.layout.lb_picker, this, true);
mPickerView = (ViewGroup) mRootView.findViewById(R.id.picker);
}
/**
* Get nth PickerColumn.
*
* @param colIndex Index of PickerColumn.
* @return PickerColumn at colIndex or null if {@link #setColumns(List)} is not called yet.
*/
public PickerColumn getColumnAt(int colIndex) {
if (mColumns == null) {
return null;
}
return mColumns.get(colIndex);
}
/**
* Get number of PickerColumns.
*
* @return Number of PickerColumns or 0 if {@link #setColumns(List)} is not called yet.
*/
public int getColumnsCount() {
if (mColumns == null) {
return 0;
}
return mColumns.size();
}
/**
* Set columns and create Views.
*
* @param columns The actual focusable columns of a picker which are scrollable if the field
* takes more than one value (e.g. for a DatePicker, day, month, and year fields
* and for TimePicker, hour, minute, and am/pm fields form the columns).
*/
public void setColumns(List<PickerColumn> columns) {
if (mSeparators.size() == 0) {
throw new IllegalStateException("Separators size is: " + mSeparators.size()
+ ". At least one separator must be provided");
} else if (mSeparators.size() == 1) {
CharSequence separator = mSeparators.get(0);
mSeparators.clear();
mSeparators.add("");
for (int i = 0; i < columns.size() - 1; i++) {
mSeparators.add(separator);
}
mSeparators.add("");
} else {
if (mSeparators.size() != (columns.size() + 1)) {
throw new IllegalStateException("Separators size: " + mSeparators.size() + " must"
+ "equal the size of columns: " + columns.size() + " + 1");
}
}
mColumnViews.clear();
mPickerView.removeAllViews();
mColumns = new ArrayList<PickerColumn>(columns);
if (mSelectedColumn > mColumns.size() - 1) {
mSelectedColumn = mColumns.size() - 1;
}
LayoutInflater inflater = LayoutInflater.from(getContext());
int totalCol = getColumnsCount();
if (!TextUtils.isEmpty(mSeparators.get(0))) {
TextView separator = (TextView) inflater.inflate(
R.layout.lb_picker_separator, mPickerView, false);
separator.setText(mSeparators.get(0));
mPickerView.addView(separator);
}
for (int i = 0; i < totalCol; i++) {
final int colIndex = i;
final VerticalGridView columnView = (VerticalGridView) inflater.inflate(
R.layout.lb_picker_column, mPickerView, false);
// we don't want VerticalGridView to receive focus.
updateColumnSize(columnView);
// always center aligned, not aligning selected item on top/bottom edge.
columnView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
// Width is dynamic, so has fixed size is false.
columnView.setHasFixedSize(false);
columnView.setFocusable(isActivated());
// Setting cache size to zero in order to rebind item views when picker widget becomes
// activated. Rebinding is necessary to update the alphas when the columns are expanded
// as a result of the picker getting activated, otherwise the cached views with the
// wrong alphas could be laid out.
columnView.setItemViewCacheSize(0);
mColumnViews.add(columnView);
// add view to root
mPickerView.addView(columnView);
if (!TextUtils.isEmpty(mSeparators.get(i + 1))) {
// add a separator if not the last element
TextView separator = (TextView) inflater.inflate(
R.layout.lb_picker_separator, mPickerView, false);
separator.setText(mSeparators.get(i + 1));
mPickerView.addView(separator);
}
columnView.setAdapter(new PickerScrollArrayAdapter(getContext(),
getPickerItemLayoutId(), getPickerItemTextViewId(), colIndex));
columnView.setOnChildViewHolderSelectedListener(mColumnChangeListener);
}
}
/**
* When column labels change or column range changes, call this function to re-populate the
* selection list. Note this function cannot be called from RecyclerView layout/scroll pass.
*
* @param columnIndex Index of column to update.
* @param column New column to update.
*/
public void setColumnAt(int columnIndex, PickerColumn column) {
mColumns.set(columnIndex, column);
VerticalGridView columnView = mColumnViews.get(columnIndex);
PickerScrollArrayAdapter adapter = (PickerScrollArrayAdapter) columnView.getAdapter();
if (adapter != null) {
adapter.notifyDataSetChanged();
}
columnView.setSelectedPosition(column.getCurrentValue() - column.getMinValue());
}
/**
* Manually set current value of a column. The function will update UI and notify listeners.
*
* @param columnIndex Index of column to update.
* @param value New value of the column.
* @param runAnimation True to scroll to the value or false otherwise.
*/
public void setColumnValue(int columnIndex, int value, boolean runAnimation) {
PickerColumn column = mColumns.get(columnIndex);
if (column.getCurrentValue() != value) {
column.setCurrentValue(value);
notifyValueChanged(columnIndex);
VerticalGridView columnView = mColumnViews.get(columnIndex);
if (columnView != null) {
int position = value - mColumns.get(columnIndex).getMinValue();
if (runAnimation) {
columnView.setSelectedPositionSmooth(position);
} else {
columnView.setSelectedPosition(position);
}
}
}
}
private void notifyValueChanged(int columnIndex) {
if (mListeners != null) {
for (int i = mListeners.size() - 1; i >= 0; i--) {
mListeners.get(i).onValueChanged(this, columnIndex);
}
}
}
/**
* Register a callback to be invoked when the picker's value has changed.
*
* @param listener The callback to ad
*/
public void addOnValueChangedListener(PickerValueListener listener) {
if (mListeners == null) {
mListeners = new ArrayList<Picker.PickerValueListener>();
}
mListeners.add(listener);
}
/**
* Remove a previously installed value changed callback
*
* @param listener The callback to remove.
*/
public void removeOnValueChangedListener(PickerValueListener listener) {
if (mListeners != null) {
mListeners.remove(listener);
}
}
void updateColumnAlpha(int colIndex, boolean animate) {
VerticalGridView column = mColumnViews.get(colIndex);
int selected = column.getSelectedPosition();
View item;
for (int i = 0; i < column.getAdapter().getItemCount(); i++) {
item = column.getLayoutManager().findViewByPosition(i);
if (item != null) {
setOrAnimateAlpha(item, (selected == i), colIndex, animate);
}
}
}
void setOrAnimateAlpha(View view, boolean selected, int colIndex,
boolean animate) {
boolean columnShownAsActivated = colIndex == mSelectedColumn || !hasFocus();
if (selected) {
// set alpha for main item (selected) in the column
if (columnShownAsActivated) {
setOrAnimateAlpha(view, animate, mFocusedAlpha, -1, mDecelerateInterpolator);
} else {
setOrAnimateAlpha(view, animate, mUnfocusedAlpha, -1, mDecelerateInterpolator);
}
} else {
// set alpha for remaining items in the column
if (columnShownAsActivated) {
setOrAnimateAlpha(view, animate, mVisibleColumnAlpha, -1, mDecelerateInterpolator);
} else {
setOrAnimateAlpha(view, animate, mInvisibleColumnAlpha, -1,
mDecelerateInterpolator);
}
}
}
private void setOrAnimateAlpha(View view, boolean animate, float destAlpha, float startAlpha,
Interpolator interpolator) {
view.animate().cancel();
if (!animate) {
view.setAlpha(destAlpha);
} else {
if (startAlpha >= 0.0f) {
// set a start alpha
view.setAlpha(startAlpha);
}
view.animate().alpha(destAlpha)
.setDuration(mAlphaAnimDuration).setInterpolator(interpolator)
.start();
}
}
/**
* Classes extending {@link Picker} can override this function to supply the
* behavior when a list has been scrolled. Subclass may call {@link #setColumnValue(int, int,
* boolean)} and or {@link #setColumnAt(int, PickerColumn)}. Subclass should not directly call
* {@link PickerColumn#setCurrentValue(int)} which does not update internal state or notify
* listeners.
*
* @param columnIndex index of which column was changed.
* @param newValue A new value desired to be set on the column.
*/
public void onColumnValueChanged(int columnIndex, int newValue) {
PickerColumn column = mColumns.get(columnIndex);
if (column.getCurrentValue() != newValue) {
column.setCurrentValue(newValue);
notifyValueChanged(columnIndex);
}
}
private float getFloat(int resourceId) {
TypedValue buffer = new TypedValue();
getContext().getResources().getValue(resourceId, buffer, true);
return buffer.getFloat();
}
static class ViewHolder extends RecyclerView.ViewHolder {
final TextView textView;
ViewHolder(View v, TextView textView) {
super(v);
this.textView = textView;
}
}
class PickerScrollArrayAdapter extends RecyclerView.Adapter<ViewHolder> {
private final int mResource;
private final int mColIndex;
private final int mTextViewResourceId;
private PickerColumn mData;
PickerScrollArrayAdapter(Context context, int resource, int textViewResourceId,
int colIndex) {
mResource = resource;
mColIndex = colIndex;
mTextViewResourceId = textViewResourceId;
mData = mColumns.get(mColIndex);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View v = inflater.inflate(mResource, parent, false);
TextView textView;
if (mTextViewResourceId != 0) {
textView = (TextView) v.findViewById(mTextViewResourceId);
} else {
textView = (TextView) v;
}
ViewHolder vh = new ViewHolder(v, textView);
return vh;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
if (holder.textView != null && mData != null) {
holder.textView.setText(mData.getLabelFor(mData.getMinValue() + position));
}
setOrAnimateAlpha(holder.itemView,
(mColumnViews.get(mColIndex).getSelectedPosition() == position),
mColIndex, false);
}
@Override
public void onViewAttachedToWindow(ViewHolder holder) {
holder.itemView.setFocusable(isActivated());
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.getCount();
}
}
private final OnChildViewHolderSelectedListener mColumnChangeListener = new
OnChildViewHolderSelectedListener() {
@Override
public void onChildViewHolderSelected(RecyclerView parent,
RecyclerView.ViewHolder child,
int position, int subposition) {
PickerScrollArrayAdapter pickerScrollArrayAdapter =
(PickerScrollArrayAdapter) parent
.getAdapter();
int colIndex = mColumnViews.indexOf(parent);
updateColumnAlpha(colIndex, true);
if (child != null) {
int newValue = mColumns.get(colIndex).getMinValue() + position;
onColumnValueChanged(colIndex, newValue);
}
}
};
@Override
public boolean dispatchKeyEvent(android.view.KeyEvent event) {
if (isActivated()) {
final int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
if (event.getAction() == KeyEvent.ACTION_UP) {
performClick();
}
break;
default:
return super.dispatchKeyEvent(event);
}
return true;
}
return super.dispatchKeyEvent(event);
}
@Override
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
int column = getSelectedColumn();
if (column < mColumnViews.size()) {
return mColumnViews.get(column).requestFocus(direction, previouslyFocusedRect);
}
return false;
}
/**
* Classes extending {@link Picker} can choose to override this method to
* supply the {@link Picker}'s column's single item height in pixels.
*/
protected int getPickerItemHeightPixels() {
return getContext().getResources().getDimensionPixelSize(R.dimen.picker_item_height);
}
private void updateColumnSize() {
for (int i = 0; i < getColumnsCount(); i++) {
updateColumnSize(mColumnViews.get(i));
}
}
private void updateColumnSize(VerticalGridView columnView) {
ViewGroup.LayoutParams lp = columnView.getLayoutParams();
float itemCount = isActivated() ? getActivatedVisibleItemCount() : getVisibleItemCount();
lp.height = (int) (getPickerItemHeightPixels() * itemCount
+ columnView.getVerticalSpacing() * (itemCount - 1));
columnView.setLayoutParams(lp);
}
private void updateItemFocusable() {
final boolean activated = isActivated();
for (int i = 0; i < getColumnsCount(); i++) {
VerticalGridView grid = mColumnViews.get(i);
for (int j = 0; j < grid.getChildCount(); j++) {
View view = grid.getChildAt(j);
view.setFocusable(activated);
}
}
}
/**
* Returns number of visible items showing in a column when it's activated. The default value
* is 3.
*
* @return Number of visible items showing in a column when it's activated.
*/
public float getActivatedVisibleItemCount() {
return mVisibleItemsActivated;
}
/**
* Changes number of visible items showing in a column when it's activated. The default value
* is 3.
*
* @param visiblePickerItems Number of visible items showing in a column when it's activated.
*/
public void setActivatedVisibleItemCount(float visiblePickerItems) {
if (visiblePickerItems <= 0) {
throw new IllegalArgumentException();
}
if (mVisibleItemsActivated != visiblePickerItems) {
mVisibleItemsActivated = visiblePickerItems;
if (isActivated()) {
updateColumnSize();
}
}
}
/**
* Returns number of visible items showing in a column when it's not activated. The default
* value is 1.
*
* @return Number of visible items showing in a column when it's not activated.
*/
public float getVisibleItemCount() {
return 1;
}
/**
* Changes number of visible items showing in a column when it's not activated. The default
* value is 1.
*
* @param pickerItems Number of visible items showing in a column when it's not activated.
*/
public void setVisibleItemCount(float pickerItems) {
if (pickerItems <= 0) {
throw new IllegalArgumentException();
}
if (mVisibleItems != pickerItems) {
mVisibleItems = pickerItems;
if (!isActivated()) {
updateColumnSize();
}
}
}
@Override
public void setActivated(boolean activated) {
if (activated == isActivated()) {
super.setActivated(activated);
return;
}
super.setActivated(activated);
boolean hadFocus = hasFocus();
int column = getSelectedColumn();
// To avoid temporary focus loss in both the following cases, we set Picker's flag to
// FOCUS_BEFORE_DESCENDANTS first, and then back to FOCUS_AFTER_DESCENDANTS once done with
// the focus logic.
// 1. When changing from activated to deactivated, the Picker should grab the focus
// back if it's focusable. However, calling requestFocus on it will transfer the focus down
// to its children if it's flag is FOCUS_AFTER_DESCENDANTS.
// 2. When changing from deactivated to activated, while setting focusable flags on each
// column VerticalGridView, that column will call requestFocus (regardless of which column
// is the selected column) since the currently focused view (Picker) has a flag of
// FOCUS_AFTER_DESCENDANTS.
setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
if (!activated && hadFocus && isFocusable()) {
// When picker widget that originally had focus is deactivated and it is focusable, we
// should not pass the focus down to the children. The Picker itself will capture focus.
requestFocus();
}
for (int i = 0; i < getColumnsCount(); i++) {
mColumnViews.get(i).setFocusable(activated);
}
updateColumnSize();
updateItemFocusable();
if (activated && hadFocus && (column >= 0)) {
mColumnViews.get(column).requestFocus();
}
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
}
@Override
public void requestChildFocus(View child, View focused) {
super.requestChildFocus(child, focused);
for (int i = 0; i < mColumnViews.size(); i++) {
if (mColumnViews.get(i).hasFocus()) {
setSelectedColumn(i);
}
}
}
/**
* Change current selected column. Picker shows multiple items on selected column if Picker has
* focus. Picker shows multiple items on all column if Picker has no focus (e.g. a Touchscreen
* screen).
*
* @param columnIndex Index of column to activate.
*/
public void setSelectedColumn(int columnIndex) {
if (mSelectedColumn != columnIndex) {
mSelectedColumn = columnIndex;
for (int i = 0; i < mColumnViews.size(); i++) {
updateColumnAlpha(i, true);
}
}
}
/**
* Get current activated column index.
*
* @return Current activated column index.
*/
public int getSelectedColumn() {
return mSelectedColumn;
}
}