Picker.java revision 71d30e1cf5514761ba8ad4bd3c8c70540d60dbd3
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 15package android.support.v17.leanback.widget.picker; 16 17import android.content.Context; 18import android.support.v17.leanback.R; 19import android.support.v17.leanback.widget.OnChildViewHolderSelectedListener; 20import android.support.v17.leanback.widget.VerticalGridView; 21import android.support.v7.widget.RecyclerView; 22import android.util.AttributeSet; 23import android.util.TypedValue; 24import android.view.LayoutInflater; 25import android.view.View; 26import android.view.ViewGroup; 27import android.view.animation.AccelerateInterpolator; 28import android.view.animation.DecelerateInterpolator; 29import android.view.animation.Interpolator; 30import android.widget.FrameLayout; 31import android.widget.LinearLayout; 32import android.widget.TextView; 33 34import java.util.ArrayList; 35import java.util.List; 36 37/** 38 * Picker is a widget showing multiple customized {@link PickerColumn}s. The PickerColumns are 39 * initialized in {@link #setColumns(ArrayList)}. You could only set columns once and not able to 40 * add or remove Column later. Call {@link #updateAdapter(int)} if the column value range or labels 41 * change. Call {@link #updateValue(int, int, boolean)} to update the current value of 42 * PickerColumn. 43 */ 44public class Picker extends FrameLayout { 45 46 public interface PickerValueListener { 47 public void onValueChanged(Picker picker, int column); 48 } 49 50 private String mSeparator; 51 private ViewGroup mRootView; 52 private ChildFocusAwareLinearLayout mPickerView; 53 private List<VerticalGridView> mColumnViews = new ArrayList<VerticalGridView>(); 54 private ArrayList<PickerColumn> mColumns; 55 56 private float mUnfocusedAlpha; 57 private float mFocusedAlpha; 58 private float mVisibleColumnAlpha; 59 private float mInvisibleColumnAlpha; 60 private int mAlphaAnimDuration; 61 private Interpolator mDecelerateInterpolator; 62 private Interpolator mAccelerateInterpolator; 63 private ArrayList<PickerValueListener> mListeners; 64 65 /** 66 * Classes extending {@link Picker} can choose to override this method to 67 * supply the separator string 68 */ 69 protected String getSeparator() { 70 return mSeparator; 71 } 72 73 /** 74 * Classes extending {@link Picker} can choose to override this method to 75 * supply the {@link Picker}'s root layout id 76 */ 77 protected int getRootLayoutId() { 78 return R.layout.lb_picker; 79 } 80 81 /** 82 * Classes extending {@link Picker} can choose to override this method to 83 * supply the {@link Picker}'s id from within the layout provided by 84 * {@link Picker#getRootLayoutId()} 85 */ 86 protected int getPickerId() { 87 return R.id.picker; 88 } 89 90 /** 91 * Classes extending {@link Picker} can choose to override this method to 92 * supply the {@link Picker}'s separator's layout id 93 */ 94 protected int getPickerSeparatorLayoutId() { 95 return R.layout.lb_picker_separator; 96 } 97 98 /** 99 * Classes extending {@link Picker} can choose to override this method to 100 * supply the {@link Picker}'s item's layout id 101 */ 102 protected int getPickerItemLayoutId() { 103 return R.layout.lb_picker_item; 104 } 105 106 /** 107 * Classes extending {@link Picker} can choose to override this method to 108 * supply the {@link Picker}'s item's {@link TextView}'s id from within the 109 * layout provided by {@link Picker#getPickerItemLayoutId()} or 0 if the 110 * layout provided by {@link Picker#getPickerItemLayoutId()} is a {link 111 * TextView}. 112 */ 113 protected int getPickerItemTextViewId() { 114 return 0; 115 } 116 117 /** 118 * Classes extending {@link Picker} can choose to override this method to 119 * supply the {@link Picker}'s column's height in pixels. 120 */ 121 protected int getPickerColumnHeightPixels() { 122 return getContext().getResources().getDimensionPixelSize(R.dimen.picker_column_height); 123 } 124 125 /** 126 * Creates a Picker widget. 127 * @param context 128 * @param attrs 129 * @param defStyleAttr 130 */ 131 public Picker(Context context, AttributeSet attrs, int defStyleAttr) { 132 super(context, attrs, defStyleAttr); 133 134 mFocusedAlpha = 1f; //getFloat(R.dimen.list_item_selected_title_text_alpha); 135 mUnfocusedAlpha = 1f; //getFloat(R.dimen.list_item_unselected_text_alpha); 136 mVisibleColumnAlpha = 0.5f; //getFloat(R.dimen.picker_item_visible_column_item_alpha); 137 mInvisibleColumnAlpha = 0f; //getFloat(R.dimen.picker_item_invisible_column_item_alpha); 138 139 mAlphaAnimDuration = 200; // mContext.getResources().getInteger(R.integer.dialog_animation_duration); 140 141 mDecelerateInterpolator = new DecelerateInterpolator(2.5F); 142 mAccelerateInterpolator = new AccelerateInterpolator(2.5F); 143 144 LayoutInflater inflater = LayoutInflater.from(getContext()); 145 mRootView = (ViewGroup) inflater.inflate(getRootLayoutId(), this, true); 146 mPickerView = (ChildFocusAwareLinearLayout) mRootView.findViewById(getPickerId()); 147 148 mPickerView.setOnChildFocusListener(mColumnGainFocusListener); 149 150 } 151 152 /** 153 * Get nth PickerColumn. 154 * @param colIndex Index of PickerColumn. 155 * @return PickerColumn at colIndex or null if {@link #setColumns(ArrayList)} is not called yet. 156 */ 157 public PickerColumn getColumnAt(int colIndex) { 158 if (mColumns == null) { 159 return null; 160 } 161 return mColumns.get(colIndex); 162 } 163 164 /** 165 * Get number of PickerColumns. 166 * @return Number of PickerColumns or 0 if {@link #setColumns(ArrayList)} is not called yet. 167 */ 168 public int getColumnsCount() { 169 if (mColumns == null) { 170 return 0; 171 } 172 return mColumns.size(); 173 } 174 175 /** 176 * Set columns and create Views. The method is only allowed to be called once. 177 * @param columns PickerColumns to be shown in the Picker. 178 */ 179 public void setColumns(ArrayList<PickerColumn> columns) { 180 if (mColumns != null) { 181 throw new IllegalStateException("columns can only be initialized once"); 182 } 183 mColumns = columns; 184 LayoutInflater inflater = LayoutInflater.from(getContext()); 185 int totalCol = getColumnsCount(); 186 for (int i = 0; i < totalCol; i++) { 187 final int colIndex = i; 188 final VerticalGridView columnView = (VerticalGridView) inflater.inflate( 189 R.layout.lb_picker_column, mPickerView, false); 190 ViewGroup.LayoutParams lp = columnView.getLayoutParams(); 191 lp.height = getPickerColumnHeightPixels(); 192 columnView.setLayoutParams(lp); 193 columnView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 194 columnView.setHasFixedSize(false); 195 mColumnViews.add(columnView); 196 197 // add view to root 198 mPickerView.addView(columnView); 199 200 // add a separator if not the last element 201 if (i != totalCol - 1 && getSeparator() != null) { 202 TextView separator = (TextView) inflater.inflate( 203 getPickerSeparatorLayoutId(), mPickerView, false); 204 separator.setText(getSeparator()); 205 mPickerView.addView(separator); 206 } 207 208 columnView.setAdapter(new PickerScrollArrayAdapter(getContext(), 209 getPickerItemLayoutId(), getPickerItemTextViewId(), colIndex)); 210 columnView.setOnChildViewHolderSelectedListener(mColumnChangeListener); 211 } 212 } 213 214 /** 215 * When column labels change or column range changes, call this function to re-populate the 216 * selection list. 217 * @param columnIndex Index of column to update. 218 */ 219 public void updateAdapter(int columnIndex) { 220 VerticalGridView columnView = mColumnViews.get(columnIndex); 221 PickerScrollArrayAdapter adapter = (PickerScrollArrayAdapter) columnView.getAdapter(); 222 if (adapter != null) { 223 adapter.notifyDataSetChanged(); 224 } 225 } 226 227 /** 228 * Manually set current value of a column. The function will update UI and notify listeners. 229 * @param columnIndex Index of column to update. 230 * @param value New value of the column. 231 * @param runAnimation True to scroll to the value or false otherwise. 232 */ 233 public void updateValue(int columnIndex, int value, boolean runAnimation) { 234 if (mColumns.get(columnIndex).setCurrentValue(value)) { 235 notifyValueChanged(columnIndex); 236 VerticalGridView columnView = mColumnViews.get(columnIndex); 237 if (columnView != null) { 238 int position = value - mColumns.get(columnIndex).getMinValue(); 239 if (runAnimation) { 240 columnView.setSelectedPositionSmooth(position); 241 } else { 242 columnView.setSelectedPosition(position); 243 } 244 } 245 } 246 } 247 248 private void notifyValueChanged(int columnIndex) { 249 if (mListeners != null) { 250 for (int i = mListeners.size() - 1; i >= 0; i--) { 251 mListeners.get(i).onValueChanged(this, columnIndex); 252 } 253 } 254 } 255 256 public void addPickerValueListener(PickerValueListener listener) { 257 if (mListeners == null) { 258 mListeners = new ArrayList<Picker.PickerValueListener>(); 259 } 260 mListeners.add(listener); 261 } 262 263 public void removePickerValueListener(PickerValueListener listener) { 264 if (mListeners != null) { 265 mListeners.remove(listener); 266 } 267 } 268 269 private void updateColumnAlpha(VerticalGridView column, boolean animateAlpha) { 270 if (column == null) { 271 return; 272 } 273 274 int selected = column.getSelectedPosition(); 275 View item; 276 boolean focused = column.hasFocus(); 277 278 for (int i = 0; i < column.getAdapter().getItemCount(); i++) { 279 item = column.getLayoutManager().findViewByPosition(i); 280 if (item != null) { 281 setOrAnimateAlpha(item, (selected == i), focused, animateAlpha); 282 } 283 } 284 } 285 286 private void setOrAnimateAlpha(View view, boolean selected, boolean focused, boolean animate) { 287 if (selected) { 288 // set alpha for main item (selected) in the column 289 if (focused) { 290 setOrAnimateAlpha(view, animate, mFocusedAlpha, -1, mDecelerateInterpolator); 291 } else { 292 setOrAnimateAlpha(view, animate, mUnfocusedAlpha, -1, mDecelerateInterpolator); 293 } 294 } else { 295 // set alpha for remaining items in the column 296 if (focused) { 297 setOrAnimateAlpha(view, animate, mVisibleColumnAlpha, -1, mDecelerateInterpolator); 298 } else { 299 setOrAnimateAlpha(view, animate, mInvisibleColumnAlpha, -1, 300 mDecelerateInterpolator); 301 } 302 } 303 } 304 305 private void setOrAnimateAlpha(View view, boolean animate, float destAlpha, float startAlpha, 306 Interpolator interpolator) { 307 view.animate().cancel(); 308 if (!animate) { 309 view.setAlpha(destAlpha); 310 } else { 311 if (startAlpha >= 0.0f) { 312 // set a start alpha 313 view.setAlpha(startAlpha); 314 } 315 view.animate().alpha(destAlpha) 316 .setDuration(mAlphaAnimDuration).setInterpolator(interpolator) 317 .start(); 318 } 319 } 320 321 /** 322 * Classes extending {@link Picker} can override this function to supply the 323 * behavior when a list has been scrolled. Subclass may call {@link #updateValue(int, int, 324 * boolean)} and or {@link #updateAdapter(int)}. Subclass should not directly call 325 * {@link PickerColumn#setCurrentValue(int)} which does not update internal state or notify 326 * listeners. 327 * @param columnIndex index of which column was changed. 328 * @param newValue A new value desired to be set on the column. 329 */ 330 public void onColumnValueChange(int columnIndex, int newValue) { 331 if (mColumns.get(columnIndex).setCurrentValue(newValue)) { 332 notifyValueChanged(columnIndex); 333 } 334 } 335 336 private float getFloat(int resourceId) { 337 TypedValue buffer = new TypedValue(); 338 getContext().getResources().getValue(resourceId, buffer, true); 339 return buffer.getFloat(); 340 } 341 342 static class ViewHolder extends RecyclerView.ViewHolder { 343 final TextView textView; 344 345 ViewHolder(View v, TextView textView) { 346 super(v); 347 this.textView = textView; 348 } 349 } 350 351 class PickerScrollArrayAdapter extends RecyclerView.Adapter<ViewHolder> { 352 353 private final int mResource; 354 private final int mColIndex; 355 private final int mTextViewResourceId; 356 private PickerColumn mData; 357 358 PickerScrollArrayAdapter(Context context, int resource, int textViewResourceId, 359 int colIndex) { 360 mResource = resource; 361 mColIndex = colIndex; 362 mTextViewResourceId = textViewResourceId; 363 mData = mColumns.get(mColIndex); 364 } 365 366 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 367 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 368 View v = inflater.inflate(mResource, parent, false); 369 TextView textView; 370 if (mTextViewResourceId != 0) { 371 textView = (TextView) v.findViewById(mTextViewResourceId); 372 } else { 373 textView = (TextView) v; 374 } 375 ViewHolder vh = new ViewHolder(v, textView); 376 return vh; 377 } 378 379 public void onBindViewHolder(ViewHolder holder, int position) { 380 if (holder.textView != null && mData != null) { 381 holder.textView.setText(mData.getValueLabelAt(mData.getMinValue() + position)); 382 } 383 setOrAnimateAlpha(holder.itemView, 384 (mColumnViews.get(mColIndex).getSelectedPosition() == position), 385 mColumnViews.get(mColIndex).hasFocus(), false); 386 } 387 388 public void setData(PickerColumn data) { 389 mData = data; 390 notifyDataSetChanged(); 391 } 392 393 public int getItemCount() { 394 return mData == null ? 0 : mData.getItemsCount(); 395 } 396 } 397 398 /** 399 * Interface for managing child focus in a ChildFocusAwareLinearLayout. 400 */ 401 interface OnChildFocusListener { 402 public boolean onPreRequestChildFocus(View child, View focused); 403 public void onRequestChildFocus(View child, View focused); 404 } 405 406 static class ChildFocusAwareLinearLayout extends LinearLayout { 407 408 409 private OnChildFocusListener mOnChildFocusListener; 410 411 public void setOnChildFocusListener(OnChildFocusListener listener) { 412 mOnChildFocusListener = listener; 413 } 414 415 public OnChildFocusListener getOnChildFocusListener() { 416 return mOnChildFocusListener; 417 } 418 419 public ChildFocusAwareLinearLayout(Context context, AttributeSet attrs) { 420 super(context, attrs); 421 } 422 423 @Override 424 public void requestChildFocus(View child, View focused) { 425 boolean preReturnedTrue = false; 426 if (mOnChildFocusListener != null) { 427 preReturnedTrue = mOnChildFocusListener.onPreRequestChildFocus(child, focused); 428 } 429 super.requestChildFocus(child, focused); 430 if (preReturnedTrue && mOnChildFocusListener != null) { 431 mOnChildFocusListener.onRequestChildFocus(child, focused); 432 } 433 } 434 } 435 436 private final OnChildViewHolderSelectedListener mColumnChangeListener = new 437 OnChildViewHolderSelectedListener() { 438 439 @Override 440 public void onChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child, 441 int position, int subposition) { 442 PickerScrollArrayAdapter pickerScrollArrayAdapter = (PickerScrollArrayAdapter) parent 443 .getAdapter(); 444 445 int colIndex = mColumnViews.indexOf(parent); 446 updateColumnAlpha((VerticalGridView) parent, parent.hasFocus()); 447 if (child != null) { 448 int newValue = mColumns.get(colIndex).getMinValue() + position; 449 onColumnValueChange(colIndex, newValue); 450 } 451 } 452 453 }; 454 455 private final OnChildFocusListener mColumnGainFocusListener = new OnChildFocusListener() { 456 @Override 457 public boolean onPreRequestChildFocus(View child, View focused) { 458 return true; 459 } 460 461 @Override 462 public void onRequestChildFocus(View child, View focused) { 463 for (int i = 0; i < mColumnViews.size(); i++) { 464 VerticalGridView column = mColumnViews.get(i); 465 updateColumnAlpha(column, column.hasFocus()); 466 } 467 } 468 }; 469 470} 471