1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.tv.settings.widget.picker; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.AnimatorSet; 22import android.animation.ObjectAnimator; 23import android.annotation.DimenRes; 24import android.app.Fragment; 25import android.content.Context; 26import android.os.Bundle; 27import android.support.v17.leanback.widget.OnChildSelectedListener; 28import android.support.v17.leanback.widget.VerticalGridView; 29import android.support.v7.widget.RecyclerView; 30import android.util.AttributeSet; 31import android.util.TypedValue; 32import android.view.KeyEvent; 33import android.view.LayoutInflater; 34import android.view.View; 35import android.view.ViewGroup; 36import android.view.ViewGroup.LayoutParams; 37import android.view.animation.AccelerateInterpolator; 38import android.view.animation.DecelerateInterpolator; 39import android.view.animation.Interpolator; 40import android.widget.LinearLayout; 41import android.widget.TextView; 42 43import com.android.tv.settings.R; 44 45import java.util.ArrayList; 46import java.util.Arrays; 47import java.util.List; 48 49/** 50 * Picker class 51 */ 52public abstract class Picker extends Fragment { 53 54 /** 55 * Object listening for adapter events. 56 */ 57 public interface ResultListener { 58 void onCommitResult(List<String> result); 59 } 60 61 private Context mContext; 62 private List<VerticalGridView> mColumnViews; 63 private ResultListener mResultListener; 64 private ArrayList<PickerColumn> mColumns = new ArrayList<>(); 65 66 private float mUnfocusedAlpha; 67 private float mFocusedAlpha; 68 private float mVisibleColumnAlpha; 69 private float mInvisibleColumnAlpha; 70 private int mAlphaAnimDuration; 71 private Interpolator mDecelerateInterpolator; 72 private Interpolator mAccelerateInterpolator; 73 private boolean mKeyDown = false; 74 private boolean mClicked = false; 75 76 /** 77 * selection result 78 */ 79 private List<String> mResult; 80 81 /** 82 * Classes extending {@link Picker} should override this method to supply 83 * the columns 84 */ 85 protected abstract ArrayList<PickerColumn> getColumns(); 86 87 /** 88 * Classes extending {@link Picker} can choose to override this method to 89 * supply the separator string 90 */ 91 protected abstract String getSeparator(); 92 93 @Override 94 public void onCreate(Bundle savedInstanceState) { 95 super.onCreate(savedInstanceState); 96 mContext = getActivity(); 97 98 mFocusedAlpha = getFloat(R.dimen.list_item_selected_title_text_alpha); 99 mUnfocusedAlpha = getFloat(R.dimen.list_item_unselected_text_alpha); 100 mVisibleColumnAlpha = getFloat(R.dimen.picker_item_visible_column_item_alpha); 101 mInvisibleColumnAlpha = getFloat(R.dimen.picker_item_invisible_column_item_alpha); 102 103 mAlphaAnimDuration = mContext.getResources().getInteger( 104 R.integer.dialog_animation_duration); 105 106 mDecelerateInterpolator = new DecelerateInterpolator(2.5F); 107 mAccelerateInterpolator = new AccelerateInterpolator(2.5F); 108 } 109 110 @Override 111 public View onCreateView(LayoutInflater inflater, ViewGroup container, 112 Bundle savedInstanceState) { 113 114 mColumns = getColumns(); 115 if (mColumns == null || mColumns.size() == 0) { 116 return null; 117 } 118 119 final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.picker, container, false); 120 final PickerLayout pickerView = (PickerLayout) rootView.findViewById(R.id.picker); 121 pickerView.setChildFocusListener(this); 122 mColumnViews = new ArrayList<>(); 123 mResult = new ArrayList<>(); 124 125 int totalCol = mColumns.size(); 126 for (int i = 0; i < totalCol; i++) { 127 final String[] col = mColumns.get(i).getItems(); 128 mResult.add(col[0]); 129 final VerticalGridView columnView = (VerticalGridView) inflater.inflate( 130 R.layout.picker_column, pickerView, false); 131 columnView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 132 mColumnViews.add(columnView); 133 columnView.setTag(i); 134 135 // add view to root 136 pickerView.addView(columnView); 137 138 // add a separator if not the last element 139 if (i != totalCol - 1 && getSeparator() != null) { 140 final TextView separator = 141 (TextView) inflater.inflate(R.layout.picker_separator, pickerView, false); 142 separator.setText(getSeparator()); 143 pickerView.addView(separator); 144 } 145 } 146 initAdapters(); 147 mColumnViews.get(0).requestFocus(); 148 149 mClicked = false; 150 mKeyDown = false; 151 152 return rootView; 153 } 154 155 private void initAdapters() { 156 final int totalCol = mColumns.size(); 157 for (int i = 0; i < totalCol; i++) { 158 VerticalGridView gridView = mColumnViews.get(i); 159 gridView.setAdapter(new Adapter(i, Arrays.asList(mColumns.get(i).getItems()))); 160 gridView.setOnKeyInterceptListener(new VerticalGridView.OnKeyInterceptListener() { 161 @Override 162 public boolean onInterceptKeyEvent(KeyEvent event) { 163 switch (event.getKeyCode()) { 164 case KeyEvent.KEYCODE_DPAD_CENTER: 165 case KeyEvent.KEYCODE_ENTER: 166 if (event.getAction() == KeyEvent.ACTION_DOWN) { 167 // We are only interested in the Key DOWN event here, 168 // because the Key UP event will generate a click, and 169 // will be handled by OnItemClickListener. 170 if (!mKeyDown) { 171 mKeyDown = true; 172 updateAllColumnsForClick(false); 173 } 174 } 175 break; 176 } 177 return false; 178 } 179 }); 180 } 181 } 182 183 protected void updateAdapter(int index, PickerColumn pickerColumn) { 184 final VerticalGridView gridView = mColumnViews.get(index); 185 final Adapter adapter = (Adapter) gridView.getAdapter(); 186 187 mColumns.set(index, pickerColumn); 188 adapter.setItems(Arrays.asList(pickerColumn.getItems())); 189 190 gridView.post(new Runnable() { 191 @Override 192 public void run() { 193 updateColumn(gridView, false, null); 194 } 195 }); 196 } 197 198 protected void updateSelection(int columnIndex, int selectedIndex) { 199 VerticalGridView columnView = mColumnViews.get(columnIndex); 200 if (columnView != null) { 201 columnView.setSelectedPosition(selectedIndex); 202 String text = mColumns.get(columnIndex).getItems()[selectedIndex]; 203 mResult.set(columnIndex, text); 204 } 205 } 206 207 public void setResultListener(ResultListener listener) { 208 mResultListener = listener; 209 } 210 211 private void updateAllColumnsForClick(boolean keyUp) { 212 final ArrayList<Animator> animList = new ArrayList<>(); 213 214 for (final VerticalGridView column : mColumnViews) { 215 final int selected = column.getSelectedPosition(); 216 217 final RecyclerView.LayoutManager manager = column.getLayoutManager(); 218 final int size = manager.getChildCount(); 219 220 for (int i = 0; i < size; i++) { 221 final View item = manager.getChildAt(i); 222 if (item != null) { 223 if (selected == i) { 224 // set alpha for main item (selected) in the column 225 if (keyUp) { 226 setOrAnimateAlphaInternal(item, true, mFocusedAlpha, mUnfocusedAlpha, 227 animList, mAccelerateInterpolator); 228 } else { 229 setOrAnimateAlphaInternal(item, true, mUnfocusedAlpha, -1, animList, 230 mDecelerateInterpolator); 231 } 232 } else if (!keyUp) { 233 // hide all non selected items on key down 234 setOrAnimateAlphaInternal(item, true, mInvisibleColumnAlpha, -1, animList, 235 mDecelerateInterpolator); 236 } 237 } 238 } 239 } 240 241 if (!animList.isEmpty()) { 242 AnimatorSet animSet = new AnimatorSet(); 243 animSet.playTogether(animList); 244 245 if (mClicked) { 246 animSet.addListener(new AnimatorListenerAdapter() { 247 @Override 248 public void onAnimationEnd(Animator animation) { 249 if (mResultListener != null) { 250 mResultListener.onCommitResult(mResult); 251 } 252 } 253 }); 254 } 255 animSet.start(); 256 } 257 } 258 259 public void childFocusChanged() { 260 final ArrayList<Animator> animList = new ArrayList<>(); 261 262 for (final VerticalGridView column : mColumnViews) { 263 updateColumn(column, column.hasFocus(), animList); 264 } 265 266 if (!animList.isEmpty()) { 267 AnimatorSet animSet = new AnimatorSet(); 268 animSet.playTogether(animList); 269 animSet.start(); 270 } 271 } 272 273 private void updateColumn(VerticalGridView column, boolean animateAlpha, 274 ArrayList<Animator> animList) { 275 if (column == null) { 276 return; 277 } 278 279 final int selected = column.getSelectedPosition(); 280 final boolean focused = column.hasFocus(); 281 282 ArrayList<Animator> localAnimList = animList; 283 if (animateAlpha && localAnimList == null) { 284 // no global animation list, create a local one for the current set 285 localAnimList = new ArrayList<>(); 286 } 287 288 // Iterate through the visible views 289 final RecyclerView.LayoutManager manager = column.getLayoutManager(); 290 final int size = manager.getChildCount(); 291 292 for (int i = 0; i < size; i++) { 293 final View item = manager.getChildAt(i); 294 if (item != null) { 295 setOrAnimateAlpha(item, (selected == column.getChildAdapterPosition(item)), focused, 296 animateAlpha, localAnimList); 297 } 298 } 299 if (animateAlpha && animList == null && !localAnimList.isEmpty()) { 300 // No global animation list, so play these start the current set of animations now 301 AnimatorSet animSet = new AnimatorSet(); 302 animSet.playTogether(localAnimList); 303 animSet.start(); 304 } 305 } 306 307 private void setOrAnimateAlpha(View view, boolean selected, boolean focused, boolean animate, 308 ArrayList<Animator> animList) { 309 if (selected) { 310 // set alpha for main item (selected) in the column 311 if ((focused && !mKeyDown) || mClicked) { 312 setOrAnimateAlphaInternal(view, animate, mFocusedAlpha, -1, animList, 313 mDecelerateInterpolator); 314 } else { 315 setOrAnimateAlphaInternal(view, animate, mUnfocusedAlpha, -1, animList, 316 mDecelerateInterpolator); 317 } 318 } else { 319 // set alpha for remaining items in the column 320 if (focused && !mClicked && !mKeyDown) { 321 setOrAnimateAlphaInternal(view, animate, mVisibleColumnAlpha, -1, animList, 322 mDecelerateInterpolator); 323 } else { 324 setOrAnimateAlphaInternal(view, animate, mInvisibleColumnAlpha, -1, animList, 325 mDecelerateInterpolator); 326 } 327 } 328 } 329 330 private void setOrAnimateAlphaInternal(View view, boolean animate, float destAlpha, 331 float startAlpha, ArrayList<Animator> animList, Interpolator interpolator) { 332 view.clearAnimation(); 333 if (!animate) { 334 view.setAlpha(destAlpha); 335 } else { 336 ObjectAnimator anim; 337 if (startAlpha >= 0.0f) { 338 // set a start alpha 339 anim = ObjectAnimator.ofFloat(view, "alpha", startAlpha, destAlpha); 340 } else { 341 // no start alpha 342 anim = ObjectAnimator.ofFloat(view, "alpha", destAlpha); 343 } 344 anim.setDuration(mAlphaAnimDuration); 345 anim.setInterpolator(interpolator); 346 if (animList != null) { 347 animList.add(anim); 348 } else { 349 anim.start(); 350 } 351 } 352 } 353 354 /** 355 * Classes extending {@link Picker} can override this function to supply the 356 * behavior when a list has been scrolled 357 */ 358 protected void onScroll(int column, View v, int position) {} 359 360 private float getFloat(@DimenRes int resourceId) { 361 TypedValue buffer = new TypedValue(); 362 mContext.getResources().getValue(resourceId, buffer, true); 363 return buffer.getFloat(); 364 } 365 366 private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { 367 private final TextView mTextView; 368 369 public ViewHolder(View itemView) { 370 super(itemView); 371 mTextView = (TextView) itemView.findViewById(R.id.list_item); 372 itemView.setOnClickListener(this); 373 } 374 375 public TextView getTextView() { 376 return mTextView; 377 } 378 379 @Override 380 public void onClick(View v) { 381 if (mKeyDown) { 382 mKeyDown = false; 383 mClicked = true; 384 updateAllColumnsForClick(true); 385 } 386 } 387 } 388 389 private class Adapter extends RecyclerView.Adapter<ViewHolder> 390 implements OnChildSelectedListener { 391 392 private final int mColumnId; 393 394 private List<String> mItems; 395 private VerticalGridView mGridView; 396 397 public Adapter(int columnId, List<String> items) { 398 mColumnId = columnId; 399 mItems = items; 400 setHasStableIds(true); 401 } 402 403 @Override 404 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 405 final View view = getLayoutInflater(null).inflate(R.layout.picker_item, parent, false); 406 return new ViewHolder(view); 407 } 408 409 @Override 410 public void onBindViewHolder(ViewHolder holder, int position) { 411 final TextView textView = holder.getTextView(); 412 textView.setText(mItems.get(position)); 413 setOrAnimateAlpha(textView, mGridView.getSelectedPosition() == position, 414 mGridView.hasFocus(), false, null); 415 } 416 417 @Override 418 public int getItemCount() { 419 return mItems.size(); 420 } 421 422 @Override 423 public long getItemId(int position) { 424 return position; 425 } 426 427 @Override 428 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 429 mGridView = (VerticalGridView) recyclerView; 430 mGridView.setOnChildSelectedListener(this); 431 } 432 433 @Override 434 public void onDetachedFromRecyclerView(RecyclerView recyclerView) { 435 mGridView = null; 436 } 437 438 @Override 439 public void onChildSelected(ViewGroup parent, View view, int position, long id) { 440 if (mGridView == null) { 441 return; 442 } 443 final ViewHolder vh = (ViewHolder) mGridView.getChildViewHolder(view); 444 final TextView textView = vh.getTextView(); 445 446 updateColumn(mGridView, mGridView.hasFocus(), null); 447 mResult.set(mColumnId, textView.getText().toString()); 448 onScroll(mColumnId, textView, position); 449 } 450 451 public void setItems(List<String> items) { 452 final List<String> oldItems = mItems; 453 mItems = items; 454 if (oldItems.size() < items.size()) { 455 notifyItemRangeInserted(oldItems.size(), oldItems.size() - items.size()); 456 } else if (items.size() < oldItems.size()) { 457 notifyItemRangeRemoved(items.size(), items.size() - oldItems.size()); 458 } 459 } 460 } 461 462 public static class PickerLayout extends LinearLayout { 463 464 private Picker mChildFocusListener; 465 466 public PickerLayout(Context context) { 467 super(context); 468 } 469 470 public PickerLayout(Context context, AttributeSet attrs) { 471 super(context, attrs); 472 } 473 474 public PickerLayout(Context context, AttributeSet attrs, int defStyleAttr) { 475 super(context, attrs, defStyleAttr); 476 } 477 478 public PickerLayout(Context context, AttributeSet attrs, int defStyleAttr, 479 int defStyleRes) { 480 super(context, attrs, defStyleAttr, defStyleRes); 481 } 482 483 @Override 484 public void requestChildFocus(View child, View focused) { 485 super.requestChildFocus(child, focused); 486 487 mChildFocusListener.childFocusChanged(); 488 } 489 490 public void setChildFocusListener(Picker childFocusListener) { 491 mChildFocusListener = childFocusListener; 492 } 493 } 494} 495