NavBarTuner.java revision 46a196e2506797e04b44944f5c2a6599276f770a
1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except 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 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15package com.android.systemui.tuner; 16 17import com.android.systemui.R; 18 19import android.annotation.Nullable; 20import android.app.AlertDialog; 21import android.app.Fragment; 22import android.content.Context; 23import android.content.DialogInterface; 24import android.content.res.ColorStateList; 25import android.content.res.TypedArray; 26import android.graphics.Canvas; 27import android.graphics.drawable.Drawable; 28import android.os.Bundle; 29import android.provider.Settings; 30import android.support.v7.widget.LinearLayoutManager; 31import android.support.v7.widget.RecyclerView; 32import android.support.v7.widget.helper.ItemTouchHelper; 33import android.util.TypedValue; 34import android.view.LayoutInflater; 35import android.view.Menu; 36import android.view.MenuInflater; 37import android.view.MenuItem; 38import android.view.MotionEvent; 39import android.view.View; 40import android.view.ViewGroup; 41import android.widget.ImageView; 42import android.widget.SeekBar; 43import android.widget.TextView; 44 45import java.util.ArrayList; 46import java.util.List; 47 48import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.SIZE_MOD_END; 49import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.SIZE_MOD_START; 50import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.extractButton; 51import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.extractSize; 52import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.BACK; 53import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.BUTTON_SEPARATOR; 54import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.GRAVITY_SEPARATOR; 55import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.HOME; 56import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.MENU_IME; 57import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.NAVSPACE; 58import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.NAV_BAR_VIEWS; 59import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.RECENT; 60 61public class NavBarTuner extends Fragment implements TunerService.Tunable { 62 63 private static final int SAVE = Menu.FIRST + 1; 64 private static final int RESET = Menu.FIRST + 2; 65 66 private NavBarAdapter mNavBarAdapter; 67 68 @Override 69 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 70 Bundle savedInstanceState) { 71 return new RecyclerView(getContext()); 72 } 73 74 @Override 75 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 76 super.onViewCreated(view, savedInstanceState); 77 RecyclerView recyclerView = (RecyclerView) view; 78 final Context context = getContext(); 79 recyclerView.setLayoutManager(new LinearLayoutManager(context)); 80 mNavBarAdapter = new NavBarAdapter(context); 81 recyclerView.setAdapter(mNavBarAdapter); 82 recyclerView.addItemDecoration(new Dividers(context)); 83 final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mNavBarAdapter.mCallbacks); 84 mNavBarAdapter.setTouchHelper(itemTouchHelper); 85 itemTouchHelper.attachToRecyclerView(recyclerView); 86 87 TunerService.get(getContext()).addTunable(this, NAV_BAR_VIEWS); 88 } 89 90 @Override 91 public void onDestroyView() { 92 super.onDestroyView(); 93 TunerService.get(getContext()).removeTunable(this); 94 } 95 96 @Override 97 public void onTuningChanged(String key, String navLayout) { 98 if (!NAV_BAR_VIEWS.equals(key)) return; 99 Context context = getContext(); 100 if (navLayout == null) { 101 navLayout = context.getString(R.string.config_navBarLayout); 102 } 103 String[] views = navLayout.split(GRAVITY_SEPARATOR); 104 String[] groups = new String[] { NavBarAdapter.START, NavBarAdapter.CENTER, 105 NavBarAdapter.END}; 106 CharSequence[] groupLabels = new String[] { getString(R.string.start), 107 getString(R.string.center), getString(R.string.end) }; 108 mNavBarAdapter.clear(); 109 for (int i = 0; i < 3; i++) { 110 mNavBarAdapter.addButton(groups[i], groupLabels[i]); 111 for (String button : views[i].split(BUTTON_SEPARATOR)) { 112 mNavBarAdapter.addButton(button, getLabel(button, context)); 113 } 114 } 115 mNavBarAdapter.addButton(NavBarAdapter.ADD, getString(R.string.add_button)); 116 setHasOptionsMenu(true); 117 } 118 119 @Override 120 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 121 super.onCreateOptionsMenu(menu, inflater); 122 // TODO: Show save button conditionally, only when there are changes. 123 menu.add(Menu.NONE, SAVE, Menu.NONE, getString(R.string.save)) 124 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 125 menu.add(Menu.NONE, RESET, Menu.NONE, getString(R.string.reset)); 126 } 127 128 @Override 129 public boolean onOptionsItemSelected(MenuItem item) { 130 if (item.getItemId() == SAVE) { 131 if (!mNavBarAdapter.hasHomeButton()) { 132 new AlertDialog.Builder(getContext()) 133 .setTitle(R.string.no_home_title) 134 .setMessage(R.string.no_home_message) 135 .setPositiveButton(android.R.string.ok, null) 136 .show(); 137 } else { 138 Settings.Secure.putString(getContext().getContentResolver(), 139 NAV_BAR_VIEWS, mNavBarAdapter.getNavString()); 140 } 141 return true; 142 } else if (item.getItemId() == RESET) { 143 Settings.Secure.putString(getContext().getContentResolver(), 144 NAV_BAR_VIEWS, null); 145 return true; 146 } 147 return super.onOptionsItemSelected(item); 148 } 149 150 private static CharSequence getLabel(String button, Context context) { 151 if (button.startsWith(HOME)) { 152 return context.getString(R.string.accessibility_home); 153 } else if (button.startsWith(BACK)) { 154 return context.getString(R.string.accessibility_back); 155 } else if (button.startsWith(RECENT)) { 156 return context.getString(R.string.accessibility_recent); 157 } else if (button.startsWith(NAVSPACE)) { 158 return context.getString(R.string.space); 159 } else if (button.startsWith(MENU_IME)) { 160 return context.getString(R.string.menu_ime); 161 } 162 return button; 163 } 164 165 private static class Holder extends RecyclerView.ViewHolder { 166 private TextView title; 167 168 public Holder(View itemView) { 169 super(itemView); 170 title = (TextView) itemView.findViewById(android.R.id.title); 171 } 172 } 173 174 private static class Dividers extends RecyclerView.ItemDecoration { 175 private final Drawable mDivider; 176 177 public Dividers(Context context) { 178 TypedValue value = new TypedValue(); 179 context.getTheme().resolveAttribute(android.R.attr.listDivider, value, true); 180 mDivider = context.getDrawable(value.resourceId); 181 } 182 183 @Override 184 public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { 185 super.onDraw(c, parent, state); 186 final int left = parent.getPaddingLeft(); 187 final int right = parent.getWidth() - parent.getPaddingRight(); 188 189 final int childCount = parent.getChildCount(); 190 for (int i = 0; i < childCount; i++) { 191 final View child = parent.getChildAt(i); 192 final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child 193 .getLayoutParams(); 194 final int top = child.getBottom() + params.bottomMargin; 195 final int bottom = top + mDivider.getIntrinsicHeight(); 196 mDivider.setBounds(left, top, right, bottom); 197 mDivider.draw(c); 198 } 199 } 200 } 201 202 private static class NavBarAdapter extends RecyclerView.Adapter<Holder> 203 implements View.OnClickListener { 204 205 private static final String START = "start"; 206 private static final String CENTER = "center"; 207 private static final String END = "end"; 208 private static final String ADD = "add"; 209 210 private static final int ADD_ID = 0; 211 private static final int BUTTON_ID = 1; 212 private static final int CATEGORY_ID = 2; 213 214 private List<String> mButtons = new ArrayList<>(); 215 private List<CharSequence> mLabels = new ArrayList<>(); 216 private int mCategoryLayout; 217 private int mButtonLayout; 218 private ItemTouchHelper mTouchHelper; 219 220 public NavBarAdapter(Context context) { 221 TypedArray attrs = context.getTheme().obtainStyledAttributes(null, 222 android.R.styleable.Preference, android.R.attr.preferenceStyle, 0); 223 mButtonLayout = attrs.getResourceId(android.R.styleable.Preference_layout, 0); 224 attrs = context.getTheme().obtainStyledAttributes(null, 225 android.R.styleable.Preference, android.R.attr.preferenceCategoryStyle, 0); 226 mCategoryLayout = attrs.getResourceId(android.R.styleable.Preference_layout, 0); 227 } 228 229 public void setTouchHelper(ItemTouchHelper itemTouchHelper) { 230 mTouchHelper = itemTouchHelper; 231 } 232 233 public void clear() { 234 mButtons.clear(); 235 mLabels.clear(); 236 notifyDataSetChanged(); 237 } 238 239 public void addButton(String button, CharSequence label) { 240 mButtons.add(button); 241 mLabels.add(label); 242 notifyItemInserted(mLabels.size() - 1); 243 } 244 245 public boolean hasHomeButton() { 246 final int N = mButtons.size(); 247 for (int i = 0; i < N; i++) { 248 if (mButtons.get(i).startsWith(HOME)) { 249 return true; 250 } 251 } 252 return false; 253 } 254 255 public String getNavString() { 256 StringBuilder builder = new StringBuilder(); 257 for (int i = 1; i < mButtons.size() - 1; i++) { 258 String button = mButtons.get(i); 259 if (button.equals(CENTER) || button.equals(END)) { 260 if (builder.length() == 0 || builder.toString().endsWith(GRAVITY_SEPARATOR)) { 261 // No start or center buttons, fill with a space. 262 builder.append(NAVSPACE); 263 } 264 builder.append(GRAVITY_SEPARATOR); 265 continue; 266 } else if (builder.length() != 0 && !builder.toString().endsWith( 267 GRAVITY_SEPARATOR)) { 268 builder.append(BUTTON_SEPARATOR); 269 } 270 builder.append(button); 271 } 272 if (builder.toString().endsWith(GRAVITY_SEPARATOR)) { 273 // No end buttons, fill with space. 274 builder.append(NAVSPACE); 275 } 276 return builder.toString(); 277 } 278 279 @Override 280 public int getItemViewType(int position) { 281 String button = mButtons.get(position); 282 if (button.equals(START) || button.equals(CENTER) || button.equals(END)) { 283 return CATEGORY_ID; 284 } 285 if (button.equals(ADD)) { 286 return ADD_ID; 287 } 288 return BUTTON_ID; 289 } 290 291 @Override 292 public Holder onCreateViewHolder(ViewGroup parent, int viewType) { 293 final Context context = parent.getContext(); 294 final LayoutInflater inflater = LayoutInflater.from(context); 295 final View view = inflater.inflate(getLayoutId(viewType), parent, false); 296 if (viewType == BUTTON_ID) { 297 inflater.inflate(R.layout.nav_control_widget, 298 (ViewGroup) view.findViewById(android.R.id.widget_frame)); 299 } 300 return new Holder(view); 301 } 302 303 private int getLayoutId(int viewType) { 304 if (viewType == CATEGORY_ID) { 305 return mCategoryLayout; 306 } 307 return mButtonLayout; 308 } 309 310 @Override 311 public void onBindViewHolder(Holder holder, int position) { 312 holder.title.setText(mLabels.get(position)); 313 if (holder.getItemViewType() == BUTTON_ID) { 314 bindButton(holder, position); 315 } else if (holder.getItemViewType() == ADD_ID) { 316 bindAdd(holder); 317 } 318 } 319 320 private void bindAdd(Holder holder) { 321 TypedValue value = new TypedValue(); 322 final Context context = holder.itemView.getContext(); 323 context.getTheme().resolveAttribute(android.R.attr.colorAccent, value, true); 324 final ImageView icon = (ImageView) holder.itemView.findViewById(android.R.id.icon); 325 icon.setImageResource(R.drawable.ic_add); 326 icon.setImageTintList(ColorStateList.valueOf(context.getColor(value.resourceId))); 327 holder.itemView.findViewById(android.R.id.summary).setVisibility(View.GONE); 328 holder.itemView.setClickable(true); 329 holder.itemView.setOnClickListener(new View.OnClickListener() { 330 @Override 331 public void onClick(View v) { 332 showAddDialog(v.getContext()); 333 } 334 }); 335 } 336 337 private void bindButton(final Holder holder, int position) { 338 holder.itemView.findViewById(android.R.id.icon_frame).setVisibility(View.GONE); 339 holder.itemView.findViewById(android.R.id.summary).setVisibility(View.GONE); 340 bindClick(holder.itemView.findViewById(R.id.close), holder); 341 bindClick(holder.itemView.findViewById(R.id.width), holder); 342 holder.itemView.findViewById(R.id.drag).setOnTouchListener(new View.OnTouchListener() { 343 @Override 344 public boolean onTouch(View v, MotionEvent event) { 345 mTouchHelper.startDrag(holder); 346 return true; 347 } 348 }); 349 } 350 351 private void showAddDialog(Context context) { 352 final String[] options = new String[] { 353 BACK, HOME, RECENT, MENU_IME, NAVSPACE, 354 }; 355 final CharSequence[] labels = new CharSequence[options.length]; 356 for (int i = 0; i < options.length; i++) { 357 labels[i] = getLabel(options[i], context); 358 } 359 new AlertDialog.Builder(context) 360 .setTitle(R.string.select_button) 361 .setItems(labels, new DialogInterface.OnClickListener() { 362 @Override 363 public void onClick(DialogInterface dialog, int which) { 364 int index = mButtons.size() - 1; 365 mButtons.add(index, options[which]); 366 mLabels.add(index, labels[which]); 367 notifyItemInserted(index); 368 } 369 }).setNegativeButton(android.R.string.cancel, null) 370 .show(); 371 } 372 373 private void bindClick(View view, Holder holder) { 374 view.setOnClickListener(this); 375 view.setTag(holder); 376 } 377 378 @Override 379 public void onClick(View v) { 380 Holder holder = (Holder) v.getTag(); 381 if (v.getId() == R.id.width) { 382 showWidthDialog(holder, v.getContext()); 383 } else if (v.getId() == R.id.close) { 384 int position = holder.getAdapterPosition(); 385 mButtons.remove(position); 386 mLabels.remove(position); 387 notifyItemRemoved(position); 388 } 389 } 390 391 private void showWidthDialog(final Holder holder, Context context) { 392 final String buttonSpec = mButtons.get(holder.getAdapterPosition()); 393 float amount = extractSize(buttonSpec); 394 final AlertDialog dialog = new AlertDialog.Builder(context) 395 .setTitle(R.string.adjust_button_width) 396 .setView(R.layout.nav_width_view) 397 .setNegativeButton(android.R.string.cancel, null).create(); 398 dialog.setButton(DialogInterface.BUTTON_POSITIVE, 399 context.getString(android.R.string.ok), 400 new DialogInterface.OnClickListener() { 401 @Override 402 public void onClick(DialogInterface d, int which) { 403 final String button = extractButton(buttonSpec); 404 SeekBar seekBar = (SeekBar) dialog.findViewById(R.id.seekbar); 405 if (seekBar.getProgress() == 75) { 406 mButtons.set(holder.getAdapterPosition(), button); 407 } else { 408 float amount = (seekBar.getProgress() + 25) / 100f; 409 mButtons.set(holder.getAdapterPosition(), button 410 + SIZE_MOD_START + amount + SIZE_MOD_END); 411 } 412 } 413 }); 414 dialog.show(); 415 SeekBar seekBar = (SeekBar) dialog.findViewById(R.id.seekbar); 416 // Range is .25 - 1.75. 417 seekBar.setMax(150); 418 seekBar.setProgress((int) ((amount - .25f) * 100)); 419 } 420 421 @Override 422 public int getItemCount() { 423 return mButtons.size(); 424 } 425 426 private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() { 427 @Override 428 public boolean isLongPressDragEnabled() { 429 return false; 430 } 431 432 @Override 433 public boolean isItemViewSwipeEnabled() { 434 return false; 435 } 436 437 @Override 438 public int getMovementFlags(RecyclerView recyclerView, 439 RecyclerView.ViewHolder viewHolder) { 440 if (viewHolder.getItemViewType() != BUTTON_ID) { 441 return makeMovementFlags(0, 0); 442 } 443 int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; 444 return makeMovementFlags(dragFlags, 0); 445 } 446 447 @Override 448 public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, 449 RecyclerView.ViewHolder target) { 450 int from = viewHolder.getAdapterPosition(); 451 int to = target.getAdapterPosition(); 452 if (to == 0) { 453 // Can't go above the top. 454 return false; 455 } 456 move(from, to, mButtons); 457 move(from, to, mLabels); 458 notifyItemMoved(from, to); 459 return true; 460 } 461 462 private <T> void move(int from, int to, List<T> list) { 463 list.add(from > to ? to : to + 1, list.get(from)); 464 list.remove(from > to ? from + 1 : from); 465 } 466 467 @Override 468 public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { 469 // Don't care. 470 } 471 }; 472 } 473} 474