Last active
September 23, 2019 02:33
-
-
Save tolmachevroman/c267024f49109955d135 to your computer and use it in GitHub Desktop.
Camera Preview to render camera capture in real time + take photo + resize it + upload it to the server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
android:layout_width="fill_parent" | |
android:layout_height="fill_parent" | |
android:background="@color/orders_grey_background"> | |
<android.support.design.widget.AppBarLayout | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" | |
app:elevation="16dp"> | |
<android.support.v7.widget.Toolbar | |
android:id="@+id/toolbar_actionbar" | |
android:layout_width="match_parent" | |
android:layout_height="?attr/actionBarSize" | |
android:background="?attr/colorPrimary" | |
app:layout_scrollFlags="scroll|enterAlways" | |
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> | |
</android.support.design.widget.AppBarLayout> | |
<!-- Camera --> | |
<FrameLayout | |
android:id="@+id/camera_preview" | |
android:layout_width="fill_parent" | |
android:layout_height="fill_parent" | |
android:layout_marginBottom="64dp" | |
android:layout_marginTop="@dimen/abc_action_bar_default_height_material" /> | |
<!-- Receipt photo --> | |
<LinearLayout | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_gravity="top|center_horizontal" | |
android:layout_marginTop="80dp" | |
android:background="@drawable/receipt_photo_background" | |
android:orientation="vertical"> | |
<TextView | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_gravity="center_horizontal" | |
android:text="@string/receipt_photo" | |
android:textColor="@color/c_white" | |
android:textSize="15sp"/> | |
<TextView | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_gravity="center_horizontal" | |
android:text="@string/refine_focus" | |
android:textColor="@color/c_white" | |
android:textSize="14sp" /> | |
</LinearLayout> | |
<!-- Take photo button --> | |
<FrameLayout | |
android:id="@+id/take_photo_button" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:background="@drawable/take_photo_background" | |
android:layout_gravity="bottom|center_horizontal" | |
android:layout_marginBottom="90dp"> | |
<ImageView | |
android:layout_width="40dp" | |
android:layout_height="40dp" | |
android:src="@drawable/ic_camera_alt_black_48dp" | |
android:layout_gravity="center"/> | |
</FrameLayout> | |
<!-- Retake photo --> | |
<TextView | |
android:id="@+id/retake_photo_button" | |
android:layout_width="match_parent" | |
android:layout_height="48dp" | |
android:layout_gravity="bottom|center_horizontal" | |
android:layout_marginBottom="64dp" | |
android:background="@drawable/retake_photo_background" | |
android:gravity="center" | |
android:text="@string/retake_photo" | |
android:textAllCaps="true" | |
android:textColor="@color/c_white" | |
android:textSize="16sp" | |
android:textStyle="bold" | |
android:visibility="gone"/> | |
<!-- Step 1 of 4 Continue --> | |
<RelativeLayout | |
android:id="@+id/step_one_of_four_layout" | |
android:layout_width="match_parent" | |
android:layout_height="64dp" | |
android:layout_gravity="bottom" | |
android:background="@color/footer_white_background" | |
android:orientation="vertical"> | |
<View | |
android:layout_width="match_parent" | |
android:layout_height="1dp" | |
android:layout_alignParentTop="true" | |
android:background="@color/orders_grey_line" /> | |
<ImageView | |
android:id="@+id/next_arrow" | |
android:layout_width="32dp" | |
android:layout_height="32dp" | |
android:layout_alignParentRight="true" | |
android:layout_centerVertical="true" | |
android:scaleType="fitXY" | |
android:src="@drawable/ic_keyboard_arrow_right_white_48dp" | |
android:tint="@color/accepted_right_arrow" /> | |
<TextView | |
android:id="@+id/next_text" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_centerVertical="true" | |
android:layout_toLeftOf="@+id/next_arrow" | |
android:fontFamily="sans-serif" | |
android:text="@string/next" | |
android:textAllCaps="true" | |
android:textColor="@color/accepted_right_arrow" | |
android:textSize="16sp" | |
android:textStyle="bold" /> | |
<TextView | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_centerVertical="true" | |
android:layout_toLeftOf="@+id/next_text" | |
android:fontFamily="sans-serif" | |
android:paddingLeft="@dimen/general_view_margin" | |
android:text="@string/step_1_of_4" | |
android:textColor="@color/step_x_of_y" | |
android:textSize="18sp" | |
android:textStyle="normal" /> | |
</RelativeLayout> | |
</android.support.design.widget.CoordinatorLayout> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Context; | |
import android.hardware.Camera; | |
import android.util.Log; | |
import android.view.SurfaceHolder; | |
import android.view.SurfaceView; | |
import java.io.IOException; | |
import java.util.List; | |
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { | |
private final String TAG = "CameraPreview"; | |
private SurfaceHolder holder; | |
private Camera camera; | |
private List<Camera.Size> supportedPreviewSizes; | |
private Camera.Size previewSize; | |
public CameraPreview(Context context, Camera camera) { | |
super(context); | |
this.camera = camera; | |
// supported preview sizes | |
supportedPreviewSizes = camera.getParameters().getSupportedPreviewSizes(); | |
// Install a SurfaceHolder.Callback so we get notified when the | |
// underlying surface is created and destroyed. | |
holder = getHolder(); | |
holder.addCallback(this); | |
// deprecated setting, but required on Android versions prior to 3.0 | |
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); | |
} | |
public void surfaceCreated(SurfaceHolder holder) { | |
// The Surface has been created, now tell the camera where to draw the preview. | |
try { | |
Camera.Parameters params = camera.getParameters(); | |
if (params.getSupportedFocusModes().contains( | |
Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { | |
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); | |
} | |
camera.setParameters(params); | |
camera.setPreviewDisplay(holder); | |
camera.stopPreview(); | |
camera.setDisplayOrientation(90); | |
camera.startPreview(); | |
} catch (IOException e) { | |
Log.d(TAG, "Error setting camera preview: " + e.getMessage()); | |
} | |
} | |
public void surfaceDestroyed(SurfaceHolder holder) { | |
// empty. Take care of releasing the Camera preview in your activity. | |
} | |
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { | |
// If your preview can change or rotate, take care of those events here. | |
// Make sure to stop the preview before resizing or reformatting it. | |
if (this.holder.getSurface() == null){ | |
// preview surface does not exist | |
return; | |
} | |
// stop preview before making changes | |
try { | |
camera.stopPreview(); | |
} catch (Exception e){ | |
// ignore: tried to stop a non-existent preview | |
} | |
// set preview size and make any resize, rotate or | |
// reformatting changes here | |
// start preview with new settings | |
try { | |
Camera.Parameters parameters = camera.getParameters(); | |
parameters.setPreviewSize(previewSize.width, previewSize.height); | |
camera.setParameters(parameters); | |
camera.setDisplayOrientation(90); | |
camera.setPreviewDisplay(holder); | |
camera.startPreview(); | |
} catch (Exception e){ | |
Log.d(TAG, "Error starting camera preview: " + e.getMessage()); | |
} | |
} | |
@Override | |
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
final int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec); | |
final int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec); | |
if (supportedPreviewSizes != null) { | |
previewSize = getOptimalPreviewSize(supportedPreviewSizes, width, height); | |
} | |
float ratio; | |
if(previewSize.height >= previewSize.width) | |
ratio = (float) previewSize.height / (float) previewSize.width; | |
else | |
ratio = (float) previewSize.width / (float) previewSize.height; | |
setMeasuredDimension(width, (int) (width * ratio)); | |
} | |
private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) { | |
final double ASPECT_TOLERANCE = 0.1; | |
double targetRatio = (double) h / w; | |
if (sizes == null) | |
return null; | |
Camera.Size optimalSize = null; | |
double minDiff = Double.MAX_VALUE; | |
int targetHeight = h; | |
for (Camera.Size size : sizes) { | |
double ratio = (double) size.height / size.width; | |
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) | |
continue; | |
if (Math.abs(size.height - targetHeight) < minDiff) { | |
optimalSize = size; | |
minDiff = Math.abs(size.height - targetHeight); | |
} | |
} | |
if (optimalSize == null) { | |
minDiff = Double.MAX_VALUE; | |
for (Camera.Size size : sizes) { | |
if (Math.abs(size.height - targetHeight) < minDiff) { | |
optimalSize = size; | |
minDiff = Math.abs(size.height - targetHeight); | |
} | |
} | |
} | |
return optimalSize; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.cornershopapp.shopper.android.ui.main; | |
import android.app.LoaderManager; | |
import android.content.ContentUris; | |
import android.content.CursorLoader; | |
import android.content.Intent; | |
import android.content.Loader; | |
import android.database.Cursor; | |
import android.graphics.Bitmap; | |
import android.graphics.BitmapFactory; | |
import android.hardware.Camera; | |
import android.os.Bundle; | |
import android.support.annotation.NonNull; | |
import android.view.MenuItem; | |
import android.view.View; | |
import android.widget.FrameLayout; | |
import android.widget.RelativeLayout; | |
import android.widget.TextView; | |
import com.cornershopapp.shopper.android.R; | |
import com.cornershopapp.shopper.android.entity.responses.SimpleResponse; | |
import com.cornershopapp.shopper.android.enums.BarcodeTypes; | |
import com.cornershopapp.shopper.android.enums.PendingRequestTypes; | |
import com.cornershopapp.shopper.android.network.RestApi; | |
import com.cornershopapp.shopper.android.services.InsertPendingRequestService; | |
import com.cornershopapp.shopper.android.sql.Order; | |
import com.cornershopapp.shopper.android.ui.BaseActivity; | |
import com.cornershopapp.shopper.android.utils.CameraPreview; | |
import com.cornershopapp.shopper.android.utils.Utils; | |
import org.parceler.Parcels; | |
import java.io.ByteArrayOutputStream; | |
import java.io.File; | |
import java.io.FileNotFoundException; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import butterknife.ButterKnife; | |
import butterknife.InjectView; | |
import retrofit.Callback; | |
import retrofit.RetrofitError; | |
import retrofit.client.Response; | |
/** | |
* Created by romantolmachev on 11/11/15. | |
*/ | |
public class TakePhotoActivity extends BaseActivity implements View.OnClickListener, | |
LoaderManager.LoaderCallbacks<Cursor> { | |
@InjectView(R.id.step_one_of_four_layout) | |
RelativeLayout stepOneOfFourLayout; | |
@InjectView(R.id.camera_preview) | |
FrameLayout cameraPreviewLayout; | |
@InjectView(R.id.take_photo_button) | |
FrameLayout takePhotoButton; | |
@InjectView(R.id.retake_photo_button) | |
TextView retakePhotoButton; | |
Camera camera; | |
CameraPreview cameraPreview; | |
File photoFile; | |
boolean photoTaken; | |
Camera.PictureCallback picture = new Camera.PictureCallback() { | |
@Override | |
public void onPictureTaken(final byte[] originalImageData, Camera camera) { | |
photoTaken = true; | |
new Thread(new Runnable() { | |
@Override | |
public void run() { | |
byte[] resizedImageData = resizeImage(originalImageData); | |
photoFile = Utils.createPublicImageFile(); | |
if (photoFile == null) { | |
System.out.println("Error creating media file, check storage permissions: "); | |
return; | |
} | |
try { | |
FileOutputStream fos = new FileOutputStream(photoFile); | |
fos.write(resizedImageData); | |
fos.close(); | |
//TODO switch to Retrofit 2.0 and cancel uploading previous screenshot | |
uploadPhotoRequest(); | |
} catch (FileNotFoundException e) { | |
System.out.println("File not found: " + e.getMessage()); | |
} catch (IOException e) { | |
System.out.println("Error accessing file: " + e.getMessage()); | |
} | |
} | |
}).start(); | |
} | |
}; | |
final int PHOTO_WIDTH = 1200; | |
final int PHOTO_HEIGHT = 1200; | |
BarcodeTypes barcodeType; | |
float total; | |
public static final int CHECKOUT = 102; | |
@Override | |
protected void onActivityResult(int requestCode, int resultCode, Intent data) { | |
super.onActivityResult(requestCode, resultCode, data); | |
if (resultCode == RESULT_OK && requestCode == CHECKOUT) { | |
setResult(RESULT_OK); | |
finish(); | |
} | |
} | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setContentView(R.layout.activity_take_photo); | |
setUpToolbarAndTitleAndHome(getString(R.string.finish_picking), R.menu.menu_accept_order); | |
ButterKnife.inject(this); | |
stepOneOfFourLayout.setOnClickListener(this); | |
takePhotoButton.setOnClickListener(this); | |
retakePhotoButton.setOnClickListener(this); | |
getLoaderManager().initLoader(1, null, this); | |
} | |
@Override | |
public void onClick(View view) { | |
switch (view.getId()) { | |
case R.id.step_one_of_four_layout: | |
if(photoTaken) { | |
Bundle args = new Bundle(); | |
args.putParcelable(getString(R.string.key_barcode), Parcels.wrap(barcodeType)); | |
args.putFloat(getString(R.string.key_order_total), total); | |
startActivityForResult(new Intent(this, ScanReceiptBarcodeActivity.class).putExtras(args), CHECKOUT); | |
} | |
break; | |
case R.id.take_photo_button: | |
camera.takePicture(null, null, picture); | |
takePhotoButton.setVisibility(View.GONE); | |
retakePhotoButton.setVisibility(View.VISIBLE); | |
break; | |
case R.id.retake_photo_button: | |
retakePhotoButton.setVisibility(View.GONE); | |
takePhotoButton.setVisibility(View.VISIBLE); | |
camera.startPreview(); | |
break; | |
} | |
} | |
@Override | |
public boolean onOptionsItemSelected(@NonNull MenuItem item) { | |
switch (item.getItemId()) { | |
case R.id.menu_call: | |
showWhomToCallDialog(); | |
return true; | |
} | |
return super.onOptionsItemSelected(item); | |
} | |
@Override | |
protected void onResume() { | |
super.onResume(); | |
camera = getCameraInstance(); | |
if(camera != null) { | |
setupCamera(); | |
} else { | |
/** | |
* Delay is necessary for cases when you go back from the ScanReceiptBarcodeActivity | |
* and need to wait a bit before Camera is released | |
*/ | |
cameraPreviewLayout.postDelayed(new Runnable() { | |
@Override | |
public void run() { | |
camera = getCameraInstance(); | |
if(camera != null) { | |
setupCamera(); | |
} | |
} | |
}, 300); | |
} | |
} | |
@Override | |
protected void onPause() { | |
super.onPause(); | |
if (camera != null) { | |
camera.setPreviewCallback(null); | |
cameraPreview.getHolder().removeCallback(cameraPreview); | |
camera.release(); | |
camera = null; | |
} | |
} | |
/** | |
* A safe way to get an instance of the Camera object. | |
*/ | |
public static Camera getCameraInstance() { | |
Camera c = null; | |
try { | |
c = Camera.open(); // attempt to get a Camera instance | |
} catch (Exception e) { | |
// Camera is not available (in use or does not exist) | |
} | |
return c; // returns null if camera is unavailable | |
} | |
byte[] resizeImage(byte[] input) { | |
Bitmap original = BitmapFactory.decodeByteArray(input, 0, input.length); | |
Bitmap resized = Bitmap.createScaledBitmap(original, PHOTO_WIDTH, PHOTO_HEIGHT, true); | |
ByteArrayOutputStream blob = new ByteArrayOutputStream(); | |
resized.compress(Bitmap.CompressFormat.JPEG, 100, blob); | |
return blob.toByteArray(); | |
} | |
void setupCamera() { | |
cameraPreview = new CameraPreview(TakePhotoActivity.this, camera); | |
cameraPreviewLayout.removeAllViews(); | |
cameraPreviewLayout.addView(cameraPreview); | |
retakePhotoButton.setVisibility(View.GONE); | |
takePhotoButton.setVisibility(View.VISIBLE); | |
} | |
@Override | |
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { | |
String[] projection = {Order._ID, Order.TOTAL, Order.SCAN_TYPE}; | |
return new CursorLoader(this, ContentUris.withAppendedId(Order.CONTENT_URI, orderId), projection, null, null, null); | |
} | |
@Override | |
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { | |
if (cursor != null && cursor.moveToFirst()) { | |
//if scan receipt is enabled | |
if (cursor.getString(cursor.getColumnIndex(Order.SCAN_TYPE)) != null) { | |
barcodeType = BarcodeTypes.valueOf(cursor.getString(cursor.getColumnIndex(Order.SCAN_TYPE))); | |
total = cursor.getFloat(cursor.getColumnIndex(Order.TOTAL)); | |
} | |
} | |
} | |
@Override | |
public void onLoaderReset(Loader<Cursor> loader) { | |
} | |
/** | |
* ================================= API REQUESTS ============================================== | |
*/ | |
private void uploadPhotoRequest() { | |
RestApi.uploadReceipt(orderId, photoFile, new Callback<SimpleResponse>() { | |
@Override | |
public void success(SimpleResponse simpleResponse, Response response) { | |
} | |
@Override | |
public void failure(RetrofitError error) { | |
handleErrors(error); | |
} | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment