Это несколько переработанная копия моей статьи на habrahabr.ru. Если у вас нет аккаунта на «Хабре», но есть вопросы по описываемой программе, вы можете задать их здесь.
Давайте напишем программу для создания своих собственных фильмов в технике Time Lapse. Завораживающее видео, снятое в этой технике с борта МКС, можно посмотреть здесь, более доступный вариант, который можно повторить с помощью описываемой программы — здесь.
Программа имеет простой интерфейс и несложный принцип работы:
• пользователь задает периодичность снимков встроенной камерой (например, 10 с) и желаемую частоту кадров генерируемого видео (например, 25 кадров в секунду);
• после нажатия кнопки «Старт» программа каждые 10 секунд делает фотографию и записывает jpg-файл на SD-карту;
• процедура повторяется до нажатия кнопки «Стоп» и «Создать видео», после чего последовательность фотографий превращается в видео файл формата Motion JPEG, который показывает отснятый материал в 250 раз (25 * 10) быстрее реальной скорости происходивших событий.
В программе два основных класса — MainActivity, занимающегося взаимодействием с пользователем и накоплением снимков и MJPEGGenerator, ответственного за превращение последовательности изображений в видео файл.
Класс MJPEGGenerator, взятый с code.google.com, был слегка переделан в связи с тем, что в Android Java отсутствует пакет java.awt.
Процедуры работы с камерой были преимущественно взяты из материала Работа с камерой в Android, где есть хорошее описание примененных решений, проблема «залипаний» камеры после лока/анлока Android-устройства была устранена благодаря stackoverflow.
Рассмотрим более подробно работу отдельных компонентов программы, не касающихся собственно камеры.
Для того, чтобы экран во время съемки не выключался, был применен механизм PowerManager.WakeLock:
1 2 3 4 5 6 7 | private PowerManager.WakeLock wl; public void onCreate(Bundle savedInstanceState) { ... PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); wl = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "DoNotDimScreen"); } |
Периодичность запуска камеры регулируется таймером:
1 2 3 4 5 6 7 8 | Timer updateTimer = new Timer(); ... updateTimer = new Timer(); updateTimer.scheduleAtFixedRate(new TimerTask() { public void run() { if ((camera != null) && (workMode == 1)) { camera.takePicture(null, null, null, MainActivity.this); }} }, 0, capturePeriod * 1000); |
Перед началом работы проверяется наличие SD-карты:
1 2 | if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) Toast.makeText(MainActivity.this, "Please mount SD card", Toast.LENGTH_LONG).show(); |
Данные, введенные пользователем, проходят предварительную обработку: запятые, введенные в качестве разделителя десятичных разрядов, заменяются на точку; проверяется ввод недопустимых не цифровых значений; значения введенного периода и частоты кадров проверяются на вхождение в допустимый диапазон:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | periodEditText.addTextChangedListener(new TextWatcher() { public void afterTextChanged(Editable s) { if (periodEditText.getText().toString().length() == 0) capturePeriod = 0; else { if (isNum(periodEditText.getText().toString().replace(',', '.'))) { float a = Float.valueOf(periodEditText.getText().toString().replace(',', '.')); capturePeriod = (int) a; } else Toast.makeText(MainActivity.this, periodEditText.getText().toString() + " - not a digit.", Toast.LENGTH_LONG).show(); }} ... if ((fps < FPSMIN) || (fps > FPSMAX)) Toast.makeText(MainActivity.this, "FPS should be " + FPSMIN + " to " + FPSMAX + " frames per second", Toast.LENGTH_LONG).show(); |
В начале работы из папки программы удаляются все файла *.jpg, оставшиеся после предыдущего сеанса:
1 2 3 4 5 6 7 8 9 | String sdPath = Environment.getExternalStorageDirectory().getPath() + "/TimeLapseFolder/"; File saveDir = new File(sdPath); if (saveDir.isDirectory()) { String[] children = saveDir.list(); for (int i = 0; i < children.length; i++) { if (children[i].endsWith(".jpg")) new File(saveDir, children[i]).delete(); }} saveDir.delete(); |
После съемки очередного кадра пользователю показывается оставшееся на карте место:
1 | modeText.setText("Work mode: capturing, " + String.valueOf(roundOneDecimal(megAvailable)) + " Mbyte available on SD card"); |
Собственно генерация видео:
1 2 3 4 5 6 7 8 9 10 11 12 | generator = new MJPEGGenerator(videofile, aviWidth, aviHeight, fps, lastPicture); for (int addpic = 1; addpic <= lastPicture; addpic++) { String numWithZeroes = intToString(addpic, 7); String curjpg = sdPath + numWithZeroes + ".jpg"; publishProgress(numWithZeroes); if (DEBUG) Log.v(TAG, "Rendering jpg sdPath = " + curjpg); Bitmap bmp = BitmapFactory.decodeFile(curjpg); generator.addImage(bmp); } |
MJPEGGenerator запускается со следующими параметрами:
videofile — название файла видео каждый раз берется новое, нумеруемое по маске TimeLapseMovieXXX.avi, чтобы сохранить файлы, отснятые ранее;
aviWidth, aviHeight — берется из свойств камеры;
fps — задается пользователем;
lastPicture — номер последней снятой фотографии.
Чтобы не подвешивать пользовательский интерфейс, генерация видео запускается в отдельном потоке AsyncTask, взаимодействующим с GUI через onProgressUpdate.
Содержимое ключевых файлов показано ниже.
MainActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 | package com.sample.timelapse; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.text.DecimalFormat; import java.util.Arrays; import java.util.Timer; import java.util.TimerTask; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.Point; import android.hardware.Camera; import android.hardware.Camera.Size; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.PowerManager; import android.os.StatFs; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.util.TypedValue; import android.view.Display; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.View.OnFocusChangeListener; import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends Activity implements SurfaceHolder.Callback, View.OnClickListener, Camera.PictureCallback, Camera.PreviewCallback { private MJPEGGenerator generator; Timer updateTimer = new Timer(); // Main shoot timer private static final int PERIODMIN = 2; // Seconds private static final int PERIODMAX = 1000; // Seconds private static final int FPSMIN = 2; private static final int FPSMAX = 30; int aviHeight = 0; // Dimensions of final video int aviWidth = 0; // Work mode // 0: Ready to start // 1: Capturing photos // 2: Ready to create video // 3: Create video private int workMode = 0; private int capturePeriod = 0; private int fps = 0; private Camera camera; private SurfaceHolder surfaceHolder; private SurfaceView preview; private static int LOGLEVEL = 2; // Set logging level private static boolean DEBUG = LOGLEVEL > 1; @SuppressWarnings("unused") private static boolean WARNING = LOGLEVEL > 0; public static final String PREFS_NAME = "MyPrefsFile"; // For save and restore preferences private static final String TAG = "MainActivity"; // Set logging tag int lastPicture = 0; // Current picture counter int lastVideo = 0; // Current video file counter int sWidth = 0; // Screen width int sHeight = 0; // Screen height int prevsWidth = 1; // Previous screen width (after previous onWindowFocusChanged) int prevsHeight = 1; // Previous screen height (after previous onWindowFocusChanged) int commentTextBottom = 0; int oldLandCommentTextBottom = 0; private TextView periodText; private TextView framerateText; private TextView totalsnapshotsText; private Button startButton; // "Start capture" private Button createButton; // "Create video" int nativeButtonColor = 0; private EditText periodEditText; // Period private TextView secondsText; private EditText fpsEditText; // Frame rate private TextView fpsText; private TextView modeText; // Show comments float roundOneDecimal(float toround) { DecimalFormat twoDForm = new DecimalFormat("#.#"); return Float.valueOf(twoDForm.format(toround)); } static String intToString(int num, int digits) { assert digits > 0 : "Invalid number of digits"; char[] zeros = new char[digits]; // Create variable length array of zeros Arrays.fill(zeros, '0'); DecimalFormat df = new DecimalFormat(String.valueOf(zeros)); // Format number as String return df.format(num); } public boolean isNum(String s) { try { Double.parseDouble(s); } catch (NumberFormatException e) { return false; } return true; } private PowerManager.WakeLock wl; // Stop screen from dimming by enforcing wake lock @Override protected void onPause() { super.onPause(); // onPause method in the parent class if (DEBUG) Log.v(TAG, "onPause"); surfaceHolder.removeCallback(this); if (camera != null) { camera.setPreviewCallback(null); camera.stopPreview(); camera.release(); camera = null; } preview.setVisibility(View.GONE); wl.release(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // onCreate method in the parent class if (DEBUG) Log.v(TAG, "onCreate"); requestWindowFeature(Window.FEATURE_NO_TITLE); // App without a title getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // App without a status bar setContentView(R.layout.activity_main); // Set user interface PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); wl = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "DoNotDimScreen"); periodText = (TextView) findViewById(R.id.periodText); // Text fields framerateText = (TextView) findViewById(R.id.framerateText); totalsnapshotsText = (TextView) findViewById(R.id.totalsnapshotsText); startButton = (Button) findViewById(R.id.startButton); // Start capture button startButton.setOnClickListener(this); createButton = (Button) findViewById(R.id.createButton); // Create video button createButton.setOnClickListener(this); nativeButtonColor = createButton.getCurrentTextColor(); createButton.setTextColor(Color.GRAY); periodEditText = (EditText) findViewById(R.id.periodEditText); // Period secondsText = (TextView) findViewById(R.id.secondsText); periodEditText.addTextChangedListener(new TextWatcher() { public void afterTextChanged(Editable s) { if (periodEditText.getText().toString().length() == 0) capturePeriod = 0; else { if (isNum(periodEditText.getText().toString().replace(',', '.'))) { float a = Float.valueOf(periodEditText.getText().toString().replace(',', '.')); capturePeriod = (int) a; } else Toast.makeText(MainActivity.this, periodEditText.getText().toString() + " - not a digit.", Toast.LENGTH_LONG).show(); } } public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void onTextChanged(CharSequence s, int start, int before, int count) { } }); fpsEditText = (EditText) findViewById(R.id.fpsEditText); // fps EditText fpsText = (TextView) findViewById(R.id.fpsText); fpsEditText.setOnFocusChangeListener(new OnFocusChangeListener() { public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus) { // Hide soft keyboard after input InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(fpsEditText.getWindowToken(), 0); } } }); fpsEditText.addTextChangedListener(new TextWatcher() { public void afterTextChanged(Editable s) { if (fpsEditText.getText().toString().length() == 0) fps = 0; else { if (isNum(fpsEditText.getText().toString().replace(',', '.'))) { float a = Float.valueOf(fpsEditText.getText().toString().replace(',', '.')); fps = (int) a; } else Toast.makeText(MainActivity.this, fpsEditText.getText().toString() + " - not a digit.", Toast.LENGTH_LONG).show(); } } public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void onTextChanged(CharSequence s, int start, int before, int count) { } }); modeText = (TextView) findViewById(R.id.modeText); // Show comments SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); // Restore preferences oldLandCommentTextBottom = settings.getInt("oldLandCommentTextBottom", 0); } @Override protected void onResume() { super.onResume(); // onResume method in the parent class if (DEBUG) Log.v(TAG, "onResume"); preview = (SurfaceView) findViewById(R.id.mSurfaceView); if (camera == null) { camera = Camera.open(); camera.startPreview(); } surfaceHolder = preview.getHolder(); surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); surfaceHolder.setSizeFromLayout(); surfaceHolder.addCallback(this); preview.setVisibility(View.VISIBLE); wl.acquire(); Size previewSize = camera.getParameters().getPreviewSize(); aviHeight = previewSize.height; aviWidth = previewSize.width; modeText.setFocusableInTouchMode(true); // Set focus (and hide soft keyboard) modeText.requestFocus(); } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { if (DEBUG) Log.v(TAG, "surfaceChanged"); try { camera.setPreviewDisplay(surfaceHolder); } catch (IOException e) { Toast.makeText(MainActivity.this, "Error 1: " + e.toString(), Toast.LENGTH_LONG).show(); } camera.startPreview(); } public void surfaceCreated(SurfaceHolder holder) { if (DEBUG) Log.v(TAG, "surfaceCreated"); try { camera.setPreviewDisplay(holder); camera.setPreviewCallback(this); } catch (IOException e) { Toast.makeText(MainActivity.this, "Error 2: " + e.toString(), Toast.LENGTH_LONG).show(); camera.release(); camera = null; } Size previewSize = camera.getParameters().getPreviewSize(); float aspect = (float) previewSize.width / previewSize.height; int previewSurfaceWidth = preview.getWidth(); LayoutParams lp = preview.getLayoutParams(); // здесь корректируем размер отображаемого preview для ландшафтного вида, чтобы не было искажений // camera.setDisplayOrientation(0); lp.width = previewSurfaceWidth; lp.height = (int) (previewSurfaceWidth / aspect); preview.setLayoutParams(lp); camera.startPreview(); } public void surfaceDestroyed(SurfaceHolder holder) { if (DEBUG) Log.v(TAG, "surfaceDestroyed"); } @SuppressLint("NewApi") @SuppressWarnings("deprecation") void getDisplaySize() { try { if (Build.VERSION.SDK_INT >= 13) { Display display = getWindowManager().getDefaultDisplay(); Point size = new Point(); display.getSize(size); sWidth = size.x; sHeight = size.y; } else { Display display = getWindowManager().getDefaultDisplay(); sWidth = display.getWidth(); sHeight = display.getHeight(); } } catch (Exception e) { Toast.makeText(MainActivity.this, "Error 3: " + e.toString(), Toast.LENGTH_LONG).show(); } } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { getDisplaySize(); if ((prevsWidth != sWidth) || (prevsHeight != sHeight)) { // If orientation changed commentTextBottom = modeText.getTop() + modeText.getHeight(); // Calculate magnification factor float heightRatio = 0; // Landscape heightRatio = (float) sHeight / (float) commentTextBottom; oldLandCommentTextBottom = commentTextBottom; if (heightRatio > 1) heightRatio = 0.7f * heightRatio; else heightRatio = heightRatio / 0.7f; // Adjust fonts periodText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * periodText.getTextSize()); periodEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * periodEditText.getTextSize()); secondsText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * secondsText.getTextSize()); framerateText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * framerateText.getTextSize()); fpsEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * fpsEditText.getTextSize()); fpsText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * fpsText.getTextSize()); totalsnapshotsText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * totalsnapshotsText.getTextSize()); modeText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * modeText.getTextSize()); // Some components have text size a little less startButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, 0.8f * heightRatio * startButton.getTextSize()); createButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, 0.8f * heightRatio * createButton.getTextSize()); // If user comment string not formed if (modeText.getText().equals(getResources().getString(R.string.longestComment))) modeText.setText(getString(R.string.modeText)); } prevsWidth = sWidth; prevsHeight = sHeight; } } public void onClick(View v) { if (v == startButton) { if (workMode == 0) { if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) Toast.makeText(MainActivity.this, "Please mount SD card", Toast.LENGTH_LONG).show(); else if ((capturePeriod < PERIODMIN) || (capturePeriod > PERIODMAX)) Toast.makeText(MainActivity.this, "Snapshots period should be " + PERIODMIN + " to " + PERIODMAX + " seconds", Toast.LENGTH_LONG) .show(); else if ((fps < FPSMIN) || (fps > FPSMAX)) Toast.makeText(MainActivity.this, "FPS should be " + FPSMIN + " to " + FPSMAX + " frames per second", Toast.LENGTH_LONG).show(); else { if (updateTimer != null) updateTimer.cancel(); try { updateTimer = new Timer(); updateTimer.scheduleAtFixedRate(new TimerTask() { public void run() { if ((camera != null) && (workMode == 1)) { camera.takePicture(null, null, null, MainActivity.this); } } }, 0, capturePeriod * 1000); } catch (Exception e) { Toast.makeText(MainActivity.this, "Error 4: " + e.toString(), Toast.LENGTH_LONG).show(); } // Delete all jpg's try { String sdPath = Environment.getExternalStorageDirectory().getPath() + "/TimeLapseFolder/"; if (DEBUG) Log.v(TAG, "Delete jpg's sdPath = " + sdPath); File saveDir = new File(sdPath); if (saveDir.isDirectory()) { String[] children = saveDir.list(); for (int i = 0; i < children.length; i++) { if (children[i].endsWith(".jpg")) new File(saveDir, children[i]).delete(); } } saveDir.delete(); } catch (Exception e) { Toast.makeText(MainActivity.this, "Error 5: " + e.toString(), Toast.LENGTH_LONG).show(); } lastPicture = 0; workMode = 1; startButton.setText("Stop capture"); modeText.setText("Work mode: capturing"); totalsnapshotsText.setText("Total snapshots: " + String.valueOf(lastPicture)); } } else if (workMode == 1) { workMode = 2; createButton.setTextColor(nativeButtonColor); startButton.setText("Start capture"); startButton.setTextColor(Color.GRAY); modeText.setText("Work mode: ready to start"); } } if (v == createButton) { if (workMode == 2) { if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) Toast.makeText(MainActivity.this, "Please mount SD card", Toast.LENGTH_LONG).show(); else if ((capturePeriod < PERIODMIN) || (capturePeriod > PERIODMAX)) Toast.makeText(MainActivity.this, "Snapshots period should be " + PERIODMIN + " to " + PERIODMAX + " seconds", Toast.LENGTH_LONG) .show(); else if ((fps < FPSMIN) || (fps > FPSMAX)) Toast.makeText(MainActivity.this, "FPS should be " + FPSMIN + " to " + FPSMAX + " frames per second", Toast.LENGTH_LONG).show(); else { workMode = 3; createButton.setTextColor(Color.GRAY); startButton.setTextColor(Color.GRAY); modeText.setText("Work mode: create video file, please wait"); new CreateMovieInBackground().execute(); } } } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // onSaveInstanceState method in the parent class if (DEBUG) Log.v(TAG, "onSaveInstanceState"); SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); SharedPreferences.Editor editor = settings.edit(); editor.putInt("oldLandCommentTextBottom", oldLandCommentTextBottom); editor.commit(); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); // onRestoreInstanceState method in the parent class if (DEBUG) Log.v(TAG, "onRestoreInstanceState"); } public void onPictureTaken(byte[] paramArrayOfByte, Camera paramCamera) { new SaveInBackground().execute(paramArrayOfByte); if (DEBUG) Log.v(TAG, "onPictureTaken"); // после того, как снимок сделан, показ превью отключается. необходимо включить его paramCamera.startPreview(); totalsnapshotsText.setText("Total snapshots: " + String.valueOf(lastPicture)); StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath()); long bytesAvailable = (long) stat.getBlockSize() * (long) stat.getAvailableBlocks(); float megAvailable = bytesAvailable / (1024.f * 1024.f); modeText.setText("Work mode: capturing, " + String.valueOf(roundOneDecimal(megAvailable)) + " Mbyte available on SD card"); } class SaveInBackground extends AsyncTask<byte[], String, String> { @Override protected String doInBackground(byte[]... arrayOfByte) { try { String sdPath = Environment.getExternalStorageDirectory().getPath() + "/TimeLapseFolder/"; File saveDir = new File(sdPath); if (!saveDir.exists()) saveDir.mkdirs(); lastPicture++; String numWithZeroes = intToString(lastPicture, 7); String curjpg = sdPath + numWithZeroes + ".jpg"; if (DEBUG) Log.v(TAG, "Save jpg sdPath = " + curjpg); FileOutputStream os = new FileOutputStream(curjpg); os.write(arrayOfByte[0]); os.close(); } catch (Exception e) { Toast.makeText(MainActivity.this, "Error 6: " + e.toString(), Toast.LENGTH_LONG).show(); } return (null); } } class CreateMovieInBackground extends AsyncTask<byte[], String, String> { protected void onProgressUpdate(String... values) { modeText.setText("Work mode: rendering " + values[0] + ".jpg"); } protected void onPostExecute(String result) { workMode = 0; totalsnapshotsText.setText("Total snapshots: 0"); lastPicture = 0; String sdPath = Environment.getExternalStorageDirectory().getPath() + "/TimeLapseFolder/"; modeText.setText("Work mode:" + sdPath + "TimeLapseMovie" + intToString(lastVideo, 3) + ".avi is rendered"); Handler handler = new Handler(); handler.postDelayed(new Runnable() { public void run() { modeText.setText("Work mode: ready to start"); startButton.setTextColor(nativeButtonColor); } }, 5000); } @Override protected String doInBackground(byte[]... arrayOfByte) { try { File videofile = null; String sdPath = Environment.getExternalStorageDirectory().getPath() + "/TimeLapseFolder/"; // Choosing a name for the file do { lastVideo++; String curavi = sdPath + "TimeLapseMovie" + intToString(lastVideo, 3) + ".avi"; if (DEBUG) Log.v(TAG, "AVI name = " + curavi); videofile = new File(curavi); } while (videofile.exists()); generator = new MJPEGGenerator(videofile, aviWidth, aviHeight, fps, lastPicture); for (int addpic = 1; addpic <= lastPicture; addpic++) { String numWithZeroes = intToString(addpic, 7); String curjpg = sdPath + numWithZeroes + ".jpg"; publishProgress(numWithZeroes); if (DEBUG) Log.v(TAG, "Rendering jpg sdPath = " + curjpg); Bitmap bmp = BitmapFactory.decodeFile(curjpg); generator.addImage(bmp); } // Delete all jpg's try { if (DEBUG) Log.v(TAG, "Delete jpg's sdPath = " + sdPath); File saveDir = new File(sdPath); if (saveDir.isDirectory()) { String[] children = saveDir.list(); for (int i = 0; i < children.length; i++) { if (children[i].endsWith(".jpg")) new File(saveDir, children[i]).delete(); } } saveDir.delete(); } catch (Exception e) { Toast.makeText(MainActivity.this, "Error 7: " + e.toString(), Toast.LENGTH_LONG).show(); } generator.finishAVI(); } catch (Exception e) { Toast.makeText(MainActivity.this, "Error 8: " + e.toString(), Toast.LENGTH_LONG).show(); } return "OK"; } } public void onPreviewFrame(byte[] paramArrayOfByte, Camera paramCamera) { } } |
MJPEGGenerator.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 | package com.sample.timelapse; // // MJPEGGenerator.java // // Created on April 17, 2006, 11:48 PM // // To change this template, choose Tools | Options and locate the template under // the Source Creation and Management node. Right-click the template and choose // Open. You can then make changes to the template in the Source Editor. // import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import android.graphics.Bitmap; // // // @author monceaux // public class MJPEGGenerator { // // Info needed for MJPEG AVI // // - size of file minus "RIFF & 4 byte file size" // int width = 0; int height = 0; double framerate = 0; int numFrames = 0; File aviFile = null; FileOutputStream aviOutput = null; FileChannel aviChannel = null; long riffOffset = 0; long aviMovieOffset = 0; AVIIndexList indexlist = null; // Creates a new instance of MJPEGGenerator public MJPEGGenerator(File aviFile, int width, int height, double framerate, int numFrames) throws Exception { this.aviFile = aviFile; this.width = width; this.height = height; this.framerate = framerate; this.numFrames = numFrames; aviOutput = new FileOutputStream(aviFile); aviChannel = aviOutput.getChannel(); RIFFHeader rh = new RIFFHeader(); aviOutput.write(rh.toBytes()); aviOutput.write(new AVIMainHeader().toBytes()); aviOutput.write(new AVIStreamList().toBytes()); aviOutput.write(new AVIStreamHeader().toBytes()); aviOutput.write(new AVIStreamFormat().toBytes()); aviOutput.write(new AVIJunk().toBytes()); aviMovieOffset = aviChannel.position(); aviOutput.write(new AVIMovieList().toBytes()); indexlist = new AVIIndexList(); } public void addImage(Bitmap image) throws Exception { byte[] fcc = new byte[] { '0', '0', 'd', 'b' }; byte[] imagedata = writeImageToBytes(image); int useLength = imagedata.length; long position = aviChannel.position(); int extra = (useLength + (int) position) % 4; if (extra > 0) useLength = useLength + extra; indexlist.addAVIIndex((int) position, useLength); aviOutput.write(fcc); aviOutput.write(intBytes(swapInt(useLength))); aviOutput.write(imagedata); if (extra > 0) { for (int i = 0; i < extra; i++) aviOutput.write(0); } imagedata = null; } public void finishAVI() throws Exception { byte[] indexlistBytes = indexlist.toBytes(); aviOutput.write(indexlistBytes); aviOutput.close(); long size = aviFile.length(); RandomAccessFile raf = new RandomAccessFile(aviFile, "rw"); raf.seek(4); raf.write(intBytes(swapInt((int) size - 8))); raf.seek(aviMovieOffset + 4); raf.write(intBytes(swapInt((int) (size - 8 - aviMovieOffset - indexlistBytes.length)))); raf.close(); } public static int swapInt(int v) { return (v >>> 24) | (v << 24) | ((v << 8) & 0x00FF0000) | ((v >> 8) & 0x0000FF00); } public static short swapShort(short v) { return (short) ((v >>> 8) | (v << 8)); } public static byte[] intBytes(int i) { byte[] b = new byte[4]; b[0] = (byte) (i >>> 24); b[1] = (byte) ((i >>> 16) & 0x000000FF); b[2] = (byte) ((i >>> 8) & 0x000000FF); b[3] = (byte) (i & 0x000000FF); return b; } public static byte[] shortBytes(short i) { byte[] b = new byte[2]; b[0] = (byte) (i >>> 8); b[1] = (byte) (i & 0x000000FF); return b; } private class RIFFHeader { public byte[] fcc = new byte[] { 'R', 'I', 'F', 'F' }; public int fileSize = 0; public byte[] fcc2 = new byte[] { 'A', 'V', 'I', ' ' }; public byte[] fcc3 = new byte[] { 'L', 'I', 'S', 'T' }; public int listSize = 200; public byte[] fcc4 = new byte[] { 'h', 'd', 'r', 'l' }; public RIFFHeader() { } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(fileSize))); baos.write(fcc2); baos.write(fcc3); baos.write(intBytes(swapInt(listSize))); baos.write(fcc4); baos.close(); return baos.toByteArray(); } } private class AVIMainHeader { // // // FOURCC fcc; DWORD cb; DWORD dwMicroSecPerFrame; DWORD dwMaxBytesPerSec; DWORD dwPaddingGranularity; DWORD // dwFlags; DWORD dwTotalFrames; DWORD dwInitialFrames; DWORD dwStreams; DWORD dwSuggestedBufferSize; DWORD // dwWidth; DWORD dwHeight; DWORD dwReserved[4]; // public byte[] fcc = new byte[] { 'a', 'v', 'i', 'h' }; public int cb = 56; public int dwMicroSecPerFrame = 0; // (1 // / // frames // per // sec) // * // 1,000,000 public int dwMaxBytesPerSec = 10000000; public int dwPaddingGranularity = 0; public int dwFlags = 65552; public int dwTotalFrames = 0; // replace // with // correct // value public int dwInitialFrames = 0; public int dwStreams = 1; public int dwSuggestedBufferSize = 0; public int dwWidth = 0; // replace // with // correct // value public int dwHeight = 0; // replace // with // correct // value public int[] dwReserved = new int[4]; public AVIMainHeader() { dwMicroSecPerFrame = (int) ((1.0 / framerate) * 1000000.0); dwWidth = width; dwHeight = height; dwTotalFrames = numFrames; } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(cb))); baos.write(intBytes(swapInt(dwMicroSecPerFrame))); baos.write(intBytes(swapInt(dwMaxBytesPerSec))); baos.write(intBytes(swapInt(dwPaddingGranularity))); baos.write(intBytes(swapInt(dwFlags))); baos.write(intBytes(swapInt(dwTotalFrames))); baos.write(intBytes(swapInt(dwInitialFrames))); baos.write(intBytes(swapInt(dwStreams))); baos.write(intBytes(swapInt(dwSuggestedBufferSize))); baos.write(intBytes(swapInt(dwWidth))); baos.write(intBytes(swapInt(dwHeight))); baos.write(intBytes(swapInt(dwReserved[0]))); baos.write(intBytes(swapInt(dwReserved[1]))); baos.write(intBytes(swapInt(dwReserved[2]))); baos.write(intBytes(swapInt(dwReserved[3]))); baos.close(); return baos.toByteArray(); } } private class AVIStreamList { public byte[] fcc = new byte[] { 'L', 'I', 'S', 'T' }; public int size = 124; public byte[] fcc2 = new byte[] { 's', 't', 'r', 'l' }; public AVIStreamList() { } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(size))); baos.write(fcc2); baos.close(); return baos.toByteArray(); } } private class AVIStreamHeader { // // FOURCC fcc; DWORD cb; FOURCC fccType; FOURCC fccHandler; DWORD dwFlags; WORD wPriority; WORD wLanguage; DWORD // dwInitialFrames; DWORD dwScale; DWORD dwRate; DWORD dwStart; DWORD dwLength; DWORD dwSuggestedBufferSize; // DWORD dwQuality; DWORD dwSampleSize; struct { short int left; short int top; short int right; short int // bottom; } rcFrame; // public byte[] fcc = new byte[] { 's', 't', 'r', 'h' }; public int cb = 64; public byte[] fccType = new byte[] { 'v', 'i', 'd', 's' }; public byte[] fccHandler = new byte[] { 'M', 'J', 'P', 'G' }; public int dwFlags = 0; public short wPriority = 0; public short wLanguage = 0; public int dwInitialFrames = 0; public int dwScale = 0; // microseconds // per // frame public int dwRate = 1000000; // dwRate // / // dwScale // = // frame // rate public int dwStart = 0; public int dwLength = 0; // num // frames public int dwSuggestedBufferSize = 0; public int dwQuality = -1; public int dwSampleSize = 0; public int left = 0; public int top = 0; public int right = 0; public int bottom = 0; public AVIStreamHeader() { dwScale = (int) ((1.0 / framerate) * 1000000.0); dwLength = numFrames; } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(cb))); baos.write(fccType); baos.write(fccHandler); baos.write(intBytes(swapInt(dwFlags))); baos.write(shortBytes(swapShort(wPriority))); baos.write(shortBytes(swapShort(wLanguage))); baos.write(intBytes(swapInt(dwInitialFrames))); baos.write(intBytes(swapInt(dwScale))); baos.write(intBytes(swapInt(dwRate))); baos.write(intBytes(swapInt(dwStart))); baos.write(intBytes(swapInt(dwLength))); baos.write(intBytes(swapInt(dwSuggestedBufferSize))); baos.write(intBytes(swapInt(dwQuality))); baos.write(intBytes(swapInt(dwSampleSize))); baos.write(intBytes(swapInt(left))); baos.write(intBytes(swapInt(top))); baos.write(intBytes(swapInt(right))); baos.write(intBytes(swapInt(bottom))); baos.close(); return baos.toByteArray(); } } private class AVIStreamFormat { // // FOURCC fcc; DWORD cb; DWORD biSize; LONG biWidth; LONG biHeight; WORD biPlanes; WORD biBitCount; DWORD // biCompression; DWORD biSizeImage; LONG biXPelsPerMeter; LONG biYPelsPerMeter; DWORD biClrUsed; DWORD // biClrImportant; // public byte[] fcc = new byte[] { 's', 't', 'r', 'f' }; public int cb = 40; public int biSize = 40; // same // as // cb public int biWidth = 0; public int biHeight = 0; public short biPlanes = 1; public short biBitCount = 24; public byte[] biCompression = new byte[] { 'M', 'J', 'P', 'G' }; public int biSizeImage = 0; // width // x // height // in // pixels public int biXPelsPerMeter = 0; public int biYPelsPerMeter = 0; public int biClrUsed = 0; public int biClrImportant = 0; public AVIStreamFormat() { biWidth = width; biHeight = height; biSizeImage = width * height; } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(cb))); baos.write(intBytes(swapInt(biSize))); baos.write(intBytes(swapInt(biWidth))); baos.write(intBytes(swapInt(biHeight))); baos.write(shortBytes(swapShort(biPlanes))); baos.write(shortBytes(swapShort(biBitCount))); baos.write(biCompression); baos.write(intBytes(swapInt(biSizeImage))); baos.write(intBytes(swapInt(biXPelsPerMeter))); baos.write(intBytes(swapInt(biYPelsPerMeter))); baos.write(intBytes(swapInt(biClrUsed))); baos.write(intBytes(swapInt(biClrImportant))); baos.close(); return baos.toByteArray(); } } private class AVIMovieList { public byte[] fcc = new byte[] { 'L', 'I', 'S', 'T' }; public int listSize = 0; public byte[] fcc2 = new byte[] { 'm', 'o', 'v', 'i' }; // 00db size jpg image data ... public AVIMovieList() { } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(listSize))); baos.write(fcc2); baos.close(); return baos.toByteArray(); } } private class AVIIndexList { public byte[] fcc = new byte[] { 'i', 'd', 'x', '1' }; public int cb = 0; public List ind = new ArrayList(); public AVIIndexList() { } @SuppressWarnings("unused") public void addAVIIndex(AVIIndex ai) { ind.add(ai); } public void addAVIIndex(int dwOffset, int dwSize) { ind.add(new AVIIndex(dwOffset, dwSize)); } public byte[] toBytes() throws Exception { cb = 16 * ind.size(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(cb))); for (int i = 0; i < ind.size(); i++) { AVIIndex in = (AVIIndex) ind.get(i); baos.write(in.toBytes()); } baos.close(); return baos.toByteArray(); } } private class AVIIndex { public byte[] fcc = new byte[] { '0', '0', 'd', 'b' }; public int dwFlags = 16; public int dwOffset = 0; public int dwSize = 0; public AVIIndex(int dwOffset, int dwSize) { this.dwOffset = dwOffset; this.dwSize = dwSize; } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(dwFlags))); baos.write(intBytes(swapInt(dwOffset))); baos.write(intBytes(swapInt(dwSize))); baos.close(); return baos.toByteArray(); } } private class AVIJunk { public byte[] fcc = new byte[] { 'J', 'U', 'N', 'K' }; public int size = 1808; public byte[] data = new byte[size]; public AVIJunk() { Arrays.fill(data, (byte) 0); } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(size))); baos.write(data); baos.close(); return baos.toByteArray(); } } private byte[] writeImageToBytes(Bitmap image) throws Exception { ByteArrayOutputStream stream = new ByteArrayOutputStream(); image.compress(Bitmap.CompressFormat.JPEG, 100, stream); stream.close(); return stream.toByteArray(); } } |
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:ads="http://schemas.android.com/apk/lib/com.google.ads" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/appBackgroundColor" > <TextView android:id="@+id/centerEmptyText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" /> <SurfaceView android:id="@+id/mSurfaceView" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_toRightOf="@+id/centerEmptyText" > </SurfaceView> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginRight="@dimen/baseui_horizontal_margin" android:layout_toLeftOf="@+id/centerEmptyText" android:orientation="vertical" > <TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" > <TableRow android:id="@+id/periodRow" android:layout_width="wrap_content" android:layout_height="wrap_content" > <TextView android:id="@+id/periodText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:focusable="true" android:focusableInTouchMode="true" android:text="@string/periodText" > <requestFocus /> </TextView> <EditText android:id="@+id/periodEditText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:ems="2" android:inputType="numberDecimal" android:singleLine="true" /> <TextView android:id="@+id/secondsText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:text="@string/seconds" /> </TableRow> <TableRow android:id="@+id/fpsRow" android:layout_width="wrap_content" android:layout_height="wrap_content" > <TextView android:id="@+id/framerateText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:focusable="true" android:focusableInTouchMode="true" android:text="@string/framerateText" /> <EditText android:id="@+id/fpsEditText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:ems="2" android:inputType="numberDecimal" android:singleLine="true" /> <TextView android:id="@+id/fpsText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:text="@string/fps" /> </TableRow> </TableLayout> <TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <Button android:id="@+id/startButton" style="?android:attr/buttonStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:layout_weight="1" android:text="@string/startButtonText" /> <Button android:id="@+id/createButton" style="?android:attr/buttonStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:layout_weight="1" android:text="@string/createButtonText" /> </LinearLayout> <TableRow android:id="@+id/totalsnapshotsRow" android:layout_width="wrap_content" android:layout_height="wrap_content" > <TextView android:id="@+id/totalsnapshotsText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:text="@string/totalsnapshotsText" /> </TableRow> </TableLayout> <TextView android:id="@+id/modeText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:text="@string/longestComment" /> </LinearLayout> </RelativeLayout> |
AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.sample.timelapse" android:installLocation="preferExternal" android:versionCode="8" android:versionName="0.8" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="15" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" /> <uses-feature android:name="there.isnt.a.vibrate.feature" android:required="false" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".MainActivity" android:label="@string/title_activity_main" android:screenOrientation="landscape" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> |