1 /* 2 * Copyright 2017 The TensorFlow Authors. All Rights Reserved. 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 17 package org.tensorflow.demo; 18 19 import android.app.UiModeManager; 20 import android.content.Context; 21 import android.content.res.AssetManager; 22 import android.content.res.Configuration; 23 import android.graphics.Bitmap; 24 import android.graphics.Bitmap.Config; 25 import android.graphics.BitmapFactory; 26 import android.graphics.Canvas; 27 import android.graphics.Color; 28 import android.graphics.Matrix; 29 import android.graphics.Paint; 30 import android.graphics.Paint.Style; 31 import android.graphics.Rect; 32 import android.graphics.Typeface; 33 import android.media.ImageReader.OnImageAvailableListener; 34 import android.os.Bundle; 35 import android.os.SystemClock; 36 import android.util.DisplayMetrics; 37 import android.util.Size; 38 import android.util.TypedValue; 39 import android.view.Display; 40 import android.view.KeyEvent; 41 import android.view.MotionEvent; 42 import android.view.View; 43 import android.view.View.OnClickListener; 44 import android.view.View.OnTouchListener; 45 import android.view.ViewGroup; 46 import android.widget.BaseAdapter; 47 import android.widget.Button; 48 import android.widget.GridView; 49 import android.widget.ImageView; 50 import android.widget.RelativeLayout; 51 import android.widget.Toast; 52 import java.io.IOException; 53 import java.io.InputStream; 54 import java.util.ArrayList; 55 import java.util.Collections; 56 import java.util.Vector; 57 import org.tensorflow.contrib.android.TensorFlowInferenceInterface; 58 import org.tensorflow.demo.OverlayView.DrawCallback; 59 import org.tensorflow.demo.env.BorderedText; 60 import org.tensorflow.demo.env.ImageUtils; 61 import org.tensorflow.demo.env.Logger; 62 import org.tensorflow.demo.R; // Explicit import needed for internal Google builds. 63 64 /** 65 * Sample activity that stylizes the camera preview according to "A Learned Representation For 66 * Artistic Style" (https://arxiv.org/abs/1610.07629) 67 */ 68 public class StylizeActivity extends CameraActivity implements OnImageAvailableListener { 69 private static final Logger LOGGER = new Logger(); 70 71 private static final String MODEL_FILE = "file:///android_asset/stylize_quantized.pb"; 72 private static final String INPUT_NODE = "input"; 73 private static final String STYLE_NODE = "style_num"; 74 private static final String OUTPUT_NODE = "transformer/expand/conv3/conv/Sigmoid"; 75 private static final int NUM_STYLES = 26; 76 77 private static final boolean SAVE_PREVIEW_BITMAP = false; 78 79 // Whether to actively manipulate non-selected sliders so that sum of activations always appears 80 // to be 1.0. The actual style input tensor will be normalized to sum to 1.0 regardless. 81 private static final boolean NORMALIZE_SLIDERS = true; 82 83 private static final float TEXT_SIZE_DIP = 12; 84 85 private static final boolean DEBUG_MODEL = false; 86 87 private static final int[] SIZES = {128, 192, 256, 384, 512, 720}; 88 89 private static final Size DESIRED_PREVIEW_SIZE = new Size(1280, 720); 90 91 // Start at a medium size, but let the user step up through smaller sizes so they don't get 92 // immediately stuck processing a large image. 93 private int desiredSizeIndex = -1; 94 private int desiredSize = 256; 95 private int initializedSize = 0; 96 97 private Integer sensorOrientation; 98 99 private long lastProcessingTimeMs; 100 private Bitmap rgbFrameBitmap = null; 101 private Bitmap croppedBitmap = null; 102 private Bitmap cropCopyBitmap = null; 103 104 private final float[] styleVals = new float[NUM_STYLES]; 105 private int[] intValues; 106 private float[] floatValues; 107 108 private int frameNum = 0; 109 110 private Bitmap textureCopyBitmap; 111 112 private Matrix frameToCropTransform; 113 private Matrix cropToFrameTransform; 114 115 private BorderedText borderedText; 116 117 private TensorFlowInferenceInterface inferenceInterface; 118 119 private int lastOtherStyle = 1; 120 121 private boolean allZero = false; 122 123 private ImageGridAdapter adapter; 124 private GridView grid; 125 126 private final OnTouchListener gridTouchAdapter = 127 new OnTouchListener() { 128 ImageSlider slider = null; 129 130 @Override 131 public boolean onTouch(final View v, final MotionEvent event) { 132 switch (event.getActionMasked()) { 133 case MotionEvent.ACTION_DOWN: 134 for (int i = 0; i < NUM_STYLES; ++i) { 135 final ImageSlider child = adapter.items[i]; 136 final Rect rect = new Rect(); 137 child.getHitRect(rect); 138 if (rect.contains((int) event.getX(), (int) event.getY())) { 139 slider = child; 140 slider.setHilighted(true); 141 } 142 } 143 break; 144 145 case MotionEvent.ACTION_MOVE: 146 if (slider != null) { 147 final Rect rect = new Rect(); 148 slider.getHitRect(rect); 149 150 final float newSliderVal = 151 (float) 152 Math.min( 153 1.0, 154 Math.max( 155 0.0, 1.0 - (event.getY() - slider.getTop()) / slider.getHeight())); 156 157 setStyle(slider, newSliderVal); 158 } 159 break; 160 161 case MotionEvent.ACTION_UP: 162 if (slider != null) { 163 slider.setHilighted(false); 164 slider = null; 165 } 166 break; 167 168 default: // fall out 169 170 } 171 return true; 172 } 173 }; 174 175 @Override onCreate(final Bundle savedInstanceState)176 public void onCreate(final Bundle savedInstanceState) { 177 super.onCreate(savedInstanceState); 178 } 179 180 @Override getLayoutId()181 protected int getLayoutId() { 182 return R.layout.camera_connection_fragment_stylize; 183 } 184 185 @Override getDesiredPreviewFrameSize()186 protected Size getDesiredPreviewFrameSize() { 187 return DESIRED_PREVIEW_SIZE; 188 } 189 getBitmapFromAsset(final Context context, final String filePath)190 public static Bitmap getBitmapFromAsset(final Context context, final String filePath) { 191 final AssetManager assetManager = context.getAssets(); 192 193 Bitmap bitmap = null; 194 try { 195 final InputStream inputStream = assetManager.open(filePath); 196 bitmap = BitmapFactory.decodeStream(inputStream); 197 } catch (final IOException e) { 198 LOGGER.e("Error opening bitmap!", e); 199 } 200 201 return bitmap; 202 } 203 204 private class ImageSlider extends ImageView { 205 private float value = 0.0f; 206 private boolean hilighted = false; 207 208 private final Paint boxPaint; 209 private final Paint linePaint; 210 ImageSlider(final Context context)211 public ImageSlider(final Context context) { 212 super(context); 213 value = 0.0f; 214 215 boxPaint = new Paint(); 216 boxPaint.setColor(Color.BLACK); 217 boxPaint.setAlpha(128); 218 219 linePaint = new Paint(); 220 linePaint.setColor(Color.WHITE); 221 linePaint.setStrokeWidth(10.0f); 222 linePaint.setStyle(Style.STROKE); 223 } 224 225 @Override onDraw(final Canvas canvas)226 public void onDraw(final Canvas canvas) { 227 super.onDraw(canvas); 228 final float y = (1.0f - value) * canvas.getHeight(); 229 230 // If all sliders are zero, don't bother shading anything. 231 if (!allZero) { 232 canvas.drawRect(0, 0, canvas.getWidth(), y, boxPaint); 233 } 234 235 if (value > 0.0f) { 236 canvas.drawLine(0, y, canvas.getWidth(), y, linePaint); 237 } 238 239 if (hilighted) { 240 canvas.drawRect(0, 0, getWidth(), getHeight(), linePaint); 241 } 242 } 243 244 @Override onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)245 protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 246 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 247 setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); 248 } 249 setValue(final float value)250 public void setValue(final float value) { 251 this.value = value; 252 postInvalidate(); 253 } 254 setHilighted(final boolean highlighted)255 public void setHilighted(final boolean highlighted) { 256 this.hilighted = highlighted; 257 this.postInvalidate(); 258 } 259 } 260 261 private class ImageGridAdapter extends BaseAdapter { 262 final ImageSlider[] items = new ImageSlider[NUM_STYLES]; 263 final ArrayList<Button> buttons = new ArrayList<>(); 264 265 { 266 final Button sizeButton = 267 new Button(StylizeActivity.this) { 268 @Override 269 protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 270 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 271 setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); 272 } 273 }; 274 sizeButton.setText("" + desiredSize); sizeButton.setOnClickListener( new OnClickListener() { @Override public void onClick(final View v) { desiredSizeIndex = (desiredSizeIndex + 1) % SIZES.length; desiredSize = SIZES[desiredSizeIndex]; sizeButton.setText("" + desiredSize); sizeButton.postInvalidate(); } })275 sizeButton.setOnClickListener( 276 new OnClickListener() { 277 @Override 278 public void onClick(final View v) { 279 desiredSizeIndex = (desiredSizeIndex + 1) % SIZES.length; 280 desiredSize = SIZES[desiredSizeIndex]; 281 sizeButton.setText("" + desiredSize); 282 sizeButton.postInvalidate(); 283 } 284 }); 285 286 final Button saveButton = 287 new Button(StylizeActivity.this) { 288 @Override 289 protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 290 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 291 setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); 292 } 293 }; 294 saveButton.setText("save"); 295 saveButton.setTextSize(12); 296 saveButton.setOnClickListener( new OnClickListener() { @Override public void onClick(final View v) { if (textureCopyBitmap != null) { ImageUtils.saveBitmap(textureCopyBitmap, "stylized" + frameNum + ".png"); Toast.makeText( StylizeActivity.this, "Saved image to: /sdcard/tensorflow/" + "stylized" + frameNum + ".png", Toast.LENGTH_LONG) .show(); } } })297 saveButton.setOnClickListener( 298 new OnClickListener() { 299 @Override 300 public void onClick(final View v) { 301 if (textureCopyBitmap != null) { 302 // TODO(andrewharp): Save as jpeg with guaranteed unique filename. 303 ImageUtils.saveBitmap(textureCopyBitmap, "stylized" + frameNum + ".png"); 304 Toast.makeText( 305 StylizeActivity.this, 306 "Saved image to: /sdcard/tensorflow/" + "stylized" + frameNum + ".png", 307 Toast.LENGTH_LONG) 308 .show(); 309 } 310 } 311 }); 312 313 buttons.add(sizeButton); 314 buttons.add(saveButton); 315 316 for (int i = 0; i < NUM_STYLES; ++i) { 317 LOGGER.v("Creating item %d", i); 318 319 if (items[i] == null) { 320 final ImageSlider slider = new ImageSlider(StylizeActivity.this); 321 final Bitmap bm = 322 getBitmapFromAsset(StylizeActivity.this, "thumbnails/style" + i + ".jpg"); 323 slider.setImageBitmap(bm); 324 325 items[i] = slider; 326 } 327 } 328 } 329 330 @Override getCount()331 public int getCount() { 332 return buttons.size() + NUM_STYLES; 333 } 334 335 @Override getItem(final int position)336 public Object getItem(final int position) { 337 if (position < buttons.size()) { 338 return buttons.get(position); 339 } else { 340 return items[position - buttons.size()]; 341 } 342 } 343 344 @Override getItemId(final int position)345 public long getItemId(final int position) { 346 return getItem(position).hashCode(); 347 } 348 349 @Override getView(final int position, final View convertView, final ViewGroup parent)350 public View getView(final int position, final View convertView, final ViewGroup parent) { 351 if (convertView != null) { 352 return convertView; 353 } 354 return (View) getItem(position); 355 } 356 } 357 358 @Override onPreviewSizeChosen(final Size size, final int rotation)359 public void onPreviewSizeChosen(final Size size, final int rotation) { 360 final float textSizePx = TypedValue.applyDimension( 361 TypedValue.COMPLEX_UNIT_DIP, TEXT_SIZE_DIP, getResources().getDisplayMetrics()); 362 borderedText = new BorderedText(textSizePx); 363 borderedText.setTypeface(Typeface.MONOSPACE); 364 365 inferenceInterface = new TensorFlowInferenceInterface(getAssets(), MODEL_FILE); 366 367 previewWidth = size.getWidth(); 368 previewHeight = size.getHeight(); 369 370 final Display display = getWindowManager().getDefaultDisplay(); 371 final int screenOrientation = display.getRotation(); 372 373 LOGGER.i("Sensor orientation: %d, Screen orientation: %d", rotation, screenOrientation); 374 375 sensorOrientation = rotation + screenOrientation; 376 377 addCallback( 378 new DrawCallback() { 379 @Override 380 public void drawCallback(final Canvas canvas) { 381 renderDebug(canvas); 382 } 383 }); 384 385 adapter = new ImageGridAdapter(); 386 grid = (GridView) findViewById(R.id.grid_layout); 387 grid.setAdapter(adapter); 388 grid.setOnTouchListener(gridTouchAdapter); 389 390 // Change UI on Android TV 391 UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE); 392 if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { 393 DisplayMetrics displayMetrics = new DisplayMetrics(); 394 getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); 395 int styleSelectorHeight = displayMetrics.heightPixels; 396 int styleSelectorWidth = displayMetrics.widthPixels - styleSelectorHeight; 397 RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(styleSelectorWidth, ViewGroup.LayoutParams.MATCH_PARENT); 398 399 // Calculate number of style in a row, so all the style can show up without scrolling 400 int numOfStylePerRow = 3; 401 while (styleSelectorWidth / numOfStylePerRow * Math.ceil((float) (adapter.getCount() - 2) / numOfStylePerRow) > styleSelectorHeight) { 402 numOfStylePerRow++; 403 } 404 grid.setNumColumns(numOfStylePerRow); 405 layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); 406 grid.setLayoutParams(layoutParams); 407 adapter.buttons.clear(); 408 } 409 410 setStyle(adapter.items[0], 1.0f); 411 } 412 setStyle(final ImageSlider slider, final float value)413 private void setStyle(final ImageSlider slider, final float value) { 414 slider.setValue(value); 415 416 if (NORMALIZE_SLIDERS) { 417 // Slider vals correspond directly to the input tensor vals, and normalization is visually 418 // maintained by remanipulating non-selected sliders. 419 float otherSum = 0.0f; 420 421 for (int i = 0; i < NUM_STYLES; ++i) { 422 if (adapter.items[i] != slider) { 423 otherSum += adapter.items[i].value; 424 } 425 } 426 427 if (otherSum > 0.0) { 428 float highestOtherVal = 0; 429 final float factor = otherSum > 0.0f ? (1.0f - value) / otherSum : 0.0f; 430 for (int i = 0; i < NUM_STYLES; ++i) { 431 final ImageSlider child = adapter.items[i]; 432 if (child == slider) { 433 continue; 434 } 435 final float newVal = child.value * factor; 436 child.setValue(newVal > 0.01f ? newVal : 0.0f); 437 438 if (child.value > highestOtherVal) { 439 lastOtherStyle = i; 440 highestOtherVal = child.value; 441 } 442 } 443 } else { 444 // Everything else is 0, so just pick a suitable slider to push up when the 445 // selected one goes down. 446 if (adapter.items[lastOtherStyle] == slider) { 447 lastOtherStyle = (lastOtherStyle + 1) % NUM_STYLES; 448 } 449 adapter.items[lastOtherStyle].setValue(1.0f - value); 450 } 451 } 452 453 final boolean lastAllZero = allZero; 454 float sum = 0.0f; 455 for (int i = 0; i < NUM_STYLES; ++i) { 456 sum += adapter.items[i].value; 457 } 458 allZero = sum == 0.0f; 459 460 // Now update the values used for the input tensor. If nothing is set, mix in everything 461 // equally. Otherwise everything is normalized to sum to 1.0. 462 for (int i = 0; i < NUM_STYLES; ++i) { 463 styleVals[i] = allZero ? 1.0f / NUM_STYLES : adapter.items[i].value / sum; 464 465 if (lastAllZero != allZero) { 466 adapter.items[i].postInvalidate(); 467 } 468 } 469 } 470 resetPreviewBuffers()471 private void resetPreviewBuffers() { 472 croppedBitmap = Bitmap.createBitmap(desiredSize, desiredSize, Config.ARGB_8888); 473 474 frameToCropTransform = ImageUtils.getTransformationMatrix( 475 previewWidth, previewHeight, 476 desiredSize, desiredSize, 477 sensorOrientation, true); 478 479 cropToFrameTransform = new Matrix(); 480 frameToCropTransform.invert(cropToFrameTransform); 481 intValues = new int[desiredSize * desiredSize]; 482 floatValues = new float[desiredSize * desiredSize * 3]; 483 initializedSize = desiredSize; 484 } 485 486 @Override processImage()487 protected void processImage() { 488 if (desiredSize != initializedSize) { 489 LOGGER.i( 490 "Initializing at size preview size %dx%d, stylize size %d", 491 previewWidth, previewHeight, desiredSize); 492 493 rgbFrameBitmap = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); 494 croppedBitmap = Bitmap.createBitmap(desiredSize, desiredSize, Config.ARGB_8888); 495 frameToCropTransform = ImageUtils.getTransformationMatrix( 496 previewWidth, previewHeight, 497 desiredSize, desiredSize, 498 sensorOrientation, true); 499 500 cropToFrameTransform = new Matrix(); 501 frameToCropTransform.invert(cropToFrameTransform); 502 intValues = new int[desiredSize * desiredSize]; 503 floatValues = new float[desiredSize * desiredSize * 3]; 504 initializedSize = desiredSize; 505 } 506 rgbFrameBitmap.setPixels(getRgbBytes(), 0, previewWidth, 0, 0, previewWidth, previewHeight); 507 final Canvas canvas = new Canvas(croppedBitmap); 508 canvas.drawBitmap(rgbFrameBitmap, frameToCropTransform, null); 509 510 // For examining the actual TF input. 511 if (SAVE_PREVIEW_BITMAP) { 512 ImageUtils.saveBitmap(croppedBitmap); 513 } 514 515 runInBackground( 516 new Runnable() { 517 @Override 518 public void run() { 519 cropCopyBitmap = Bitmap.createBitmap(croppedBitmap); 520 final long startTime = SystemClock.uptimeMillis(); 521 stylizeImage(croppedBitmap); 522 lastProcessingTimeMs = SystemClock.uptimeMillis() - startTime; 523 textureCopyBitmap = Bitmap.createBitmap(croppedBitmap); 524 requestRender(); 525 readyForNextImage(); 526 } 527 }); 528 if (desiredSize != initializedSize) { 529 resetPreviewBuffers(); 530 } 531 } 532 stylizeImage(final Bitmap bitmap)533 private void stylizeImage(final Bitmap bitmap) { 534 ++frameNum; 535 bitmap.getPixels(intValues, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); 536 537 if (DEBUG_MODEL) { 538 // Create a white square that steps through a black background 1 pixel per frame. 539 final int centerX = (frameNum + bitmap.getWidth() / 2) % bitmap.getWidth(); 540 final int centerY = bitmap.getHeight() / 2; 541 final int squareSize = 10; 542 for (int i = 0; i < intValues.length; ++i) { 543 final int x = i % bitmap.getWidth(); 544 final int y = i / bitmap.getHeight(); 545 final float val = 546 Math.abs(x - centerX) < squareSize && Math.abs(y - centerY) < squareSize ? 1.0f : 0.0f; 547 floatValues[i * 3] = val; 548 floatValues[i * 3 + 1] = val; 549 floatValues[i * 3 + 2] = val; 550 } 551 } else { 552 for (int i = 0; i < intValues.length; ++i) { 553 final int val = intValues[i]; 554 floatValues[i * 3] = ((val >> 16) & 0xFF) / 255.0f; 555 floatValues[i * 3 + 1] = ((val >> 8) & 0xFF) / 255.0f; 556 floatValues[i * 3 + 2] = (val & 0xFF) / 255.0f; 557 } 558 } 559 560 // Copy the input data into TensorFlow. 561 LOGGER.i("Width: %s , Height: %s", bitmap.getWidth(), bitmap.getHeight()); 562 inferenceInterface.feed( 563 INPUT_NODE, floatValues, 1, bitmap.getWidth(), bitmap.getHeight(), 3); 564 inferenceInterface.feed(STYLE_NODE, styleVals, NUM_STYLES); 565 566 inferenceInterface.run(new String[] {OUTPUT_NODE}, isDebug()); 567 inferenceInterface.fetch(OUTPUT_NODE, floatValues); 568 569 for (int i = 0; i < intValues.length; ++i) { 570 intValues[i] = 571 0xFF000000 572 | (((int) (floatValues[i * 3] * 255)) << 16) 573 | (((int) (floatValues[i * 3 + 1] * 255)) << 8) 574 | ((int) (floatValues[i * 3 + 2] * 255)); 575 } 576 577 bitmap.setPixels(intValues, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); 578 } 579 renderDebug(final Canvas canvas)580 private void renderDebug(final Canvas canvas) { 581 // TODO(andrewharp): move result display to its own View instead of using debug overlay. 582 final Bitmap texture = textureCopyBitmap; 583 if (texture != null) { 584 final Matrix matrix = new Matrix(); 585 final float scaleFactor = 586 DEBUG_MODEL 587 ? 4.0f 588 : Math.min( 589 (float) canvas.getWidth() / texture.getWidth(), 590 (float) canvas.getHeight() / texture.getHeight()); 591 matrix.postScale(scaleFactor, scaleFactor); 592 canvas.drawBitmap(texture, matrix, new Paint()); 593 } 594 595 if (!isDebug()) { 596 return; 597 } 598 599 final Bitmap copy = cropCopyBitmap; 600 if (copy == null) { 601 return; 602 } 603 604 canvas.drawColor(0x55000000); 605 606 final Matrix matrix = new Matrix(); 607 final float scaleFactor = 2; 608 matrix.postScale(scaleFactor, scaleFactor); 609 matrix.postTranslate( 610 canvas.getWidth() - copy.getWidth() * scaleFactor, 611 canvas.getHeight() - copy.getHeight() * scaleFactor); 612 canvas.drawBitmap(copy, matrix, new Paint()); 613 614 final Vector<String> lines = new Vector<>(); 615 616 final String[] statLines = inferenceInterface.getStatString().split("\n"); 617 Collections.addAll(lines, statLines); 618 619 lines.add(""); 620 621 lines.add("Frame: " + previewWidth + "x" + previewHeight); 622 lines.add("Crop: " + copy.getWidth() + "x" + copy.getHeight()); 623 lines.add("View: " + canvas.getWidth() + "x" + canvas.getHeight()); 624 lines.add("Rotation: " + sensorOrientation); 625 lines.add("Inference time: " + lastProcessingTimeMs + "ms"); 626 lines.add("Desired size: " + desiredSize); 627 lines.add("Initialized size: " + initializedSize); 628 629 borderedText.drawLines(canvas, 10, canvas.getHeight() - 10, lines); 630 } 631 632 @Override onKeyDown(int keyCode, KeyEvent event)633 public boolean onKeyDown(int keyCode, KeyEvent event) { 634 int moveOffset = 0; 635 switch (keyCode) { 636 case KeyEvent.KEYCODE_DPAD_LEFT: 637 moveOffset = -1; 638 break; 639 case KeyEvent.KEYCODE_DPAD_RIGHT: 640 moveOffset = 1; 641 break; 642 case KeyEvent.KEYCODE_DPAD_UP: 643 moveOffset = -1 * grid.getNumColumns(); 644 break; 645 case KeyEvent.KEYCODE_DPAD_DOWN: 646 moveOffset = grid.getNumColumns(); 647 break; 648 default: 649 return super.onKeyDown(keyCode, event); 650 } 651 652 // get the highest selected style 653 int currentSelect = 0; 654 float highestValue = 0; 655 for (int i = 0; i < adapter.getCount(); i++) { 656 if (adapter.items[i].value > highestValue) { 657 currentSelect = i; 658 highestValue = adapter.items[i].value; 659 } 660 } 661 setStyle(adapter.items[(currentSelect + moveOffset + adapter.getCount()) % adapter.getCount()], 1); 662 663 return true; 664 } 665 } 666