Introduction
The basic ideas are very simple:
- Show camera preview
- Add rectangle view to camera preview (boundaries of new crop image)
- Crop image and save result to file
Using the Code
Let's start! Create a new project in Android studio (I used version 3.2.1) or you can download the source files and choose: File-New-Import project. Add to build.gradle
app level:
implementation 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
I use Butterknife
library, it's very useful. Also, we need camera and write permissions, so add it to AndroidManifest.xml.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" />
We need 2 fragments, one for camera preview and one to show cropped image. Create new fragment -ImageFragment
(File-New-Fragment-Fragment(blank) -add TextView
and Imageview
to layout-xml file:
="1.0"="utf-8"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ImageFragment">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/res_photo_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/res_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_background" />
</LinearLayout>
</FrameLayout>
Add some code to ImageFragment
class, it simply show image
and textview
information:
public class ImageFragment extends Fragment {
private Bitmap bitmap;
@BindView(R.id.res_photo)
ImageView resPhoto;
@BindView(R.id.res_photo_size)
TextView resPhotoSize;
public void imageSetupFragment(Bitmap bitmap) {
if (bitmap != null) {
this.bitmap = bitmap;
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setRetainInstance(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_image, container, false);
ButterKnife.bind(this, view);
if (bitmap != null) {
resPhoto.setImageBitmap(bitmap);
String info = "image with:" + bitmap.getWidth() + "\n" +
"image height:" + bitmap.getHeight();
resPhotoSize.setText(info);
}
return view;
}
}
Create second fragment -PhotoFragment
(File-New-Fragment
-Fragment
(blank) - add some components to layout-xml file. Main is SurfaceView
(for camera preview) and View
(border for cropping).
="1.0"="utf-8"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/frame_surface_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/preview_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/camera_preview_surface"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" />
<View
android:id="@+id/border_camera"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_centerInParent="true"
android:layout_marginStart="50dp"
android:layout_marginEnd="50dp"
android:background="@drawable/border" />
<TextView
android:id="@+id/res_border_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:text="size"
android:textColor="@color/colorAccent" />
<Button
android:id="@+id/make_photo_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:background="@drawable/photo_button"
android:text="photo" />
</RelativeLayout>
</LinearLayout>
</FrameLayout>
I used a custom button (green button), it's simple code but great view! To do this, you need to create a new XML file in Res-drawable- like photo_button.xml, set up shape, colors as you need or use some web resources, like this: http://angrytools.com/android/button/.
="1.0"="utf-8"
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="20dp"
android:topLeftRadius="20dp"
android:topRightRadius="0dp" />
<gradient
android:angle="45"
android:centerColor="#47A891"
android:centerX="35%"
android:endColor="#000000"
android:startColor="#E8E8E8"
android:type="linear" />
<size
android:width="100dp"
android:height="60dp" />
<stroke
android:width="3dp"
android:color="#16875A" />
</shape>
and then set background button to this:
android:background="@drawable/photo_button"
Also, we need crop border - it will be a simple rectangle, to do this, you need to create a new XML file in Res-drawable- like border.xml:
="1.0"="utf-8"
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:width="2dp" android:color="#C4CDD5" />
<corners android:radius="10dp" />
<padding android:left="2dp" android:top="2dp"
android:right="2dp" android:bottom="2dp" />
</shape>
and set view-background
to:
android:background="@drawable/border" />
Move next, PhotoFragment
class. We can't use standard intent to make a photo, we need custom functions, so we can use Camera
class - it's deprecated, but still works nice, so let's use it. The Camera
class is used to set image capture settings, start/stop preview, snap pictures, and retrieve frames for encoding for video. This class is a client for the Camera
service, which manages the actual camera hardware.
To control preview, we need to use SurfaceHolder.Callback
. This abstract interface
is to hold a display surface. Allows you to control the surface size and format, edit the pixels in the surface, and monitor changes to the surface.
public class PhotoFragment extends Fragment implements SurfaceHolder.Callback
{}
and implements some methods:
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
Like this:
@Override
public void surfaceCreated(SurfaceHolder holder) {
camera = Camera.open();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (previewing) {
camera.stopPreview();
previewing = false;
}
if (camera != null) {
try {
Camera.Parameters parameters = camera.getParameters();
List<Camera.Size> previewSizes = parameters.getSupportedPreviewSizes();
previewSizeOptimal = getOptimalPreviewSize(previewSizes, parameters.getPictureSize().width,
parameters.getPictureSize().height);
if (previewSizeOptimal != null) {
parameters.setPreviewSize(previewSizeOptimal.width, previewSizeOptimal.height);
}
if (camera.getParameters().getFocusMode().contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
}
if (camera.getParameters().getFlashMode().contains(Camera.Parameters.FLASH_MODE_AUTO)) {
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
}
camera.setParameters(parameters);
Display display = ((WindowManager) context.getSystemService
(Context.WINDOW_SERVICE)).getDefaultDisplay();
if (display.getRotation() == Surface.ROTATION_0) {
camera.setDisplayOrientation(90);
} else if (display.getRotation() == Surface.ROTATION_270) {
camera.setDisplayOrientation(180);
}
int x1 = previewLayout.getWidth();
int y1 = previewLayout.getHeight();
int x2 = borderCamera.getWidth();
int y2 = borderCamera.getHeight();
String info = "Preview width:" + String.valueOf(x1) + "\n" +
"Preview height:" + String.valueOf(y1) + "\n" +
"Border width:" + String.valueOf(x2) +
"\n" + "Border height:" + String.valueOf(y2);
resBorderSizeTV.setText(info);
camera.setPreviewDisplay(surfaceHolder);
camera.startPreview();
previewing = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
camera.stopPreview();
camera.release();
camera = null;
previewing = false;
}
Very important to set correct size of camera preview, if aspect ratio of image doesn't fit to preview camera size - cropped image will have incorrect size.
public Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
final double ASPECT_TOLERANCE = 0.1;
double targetRatio = (double) w / h;
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.width / size.height;
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;
}
Define some functions to take picture:
@OnClick(R.id.make_photo_button)
void makePhoto() {
if (camera != null) {
camera.takePicture(myShutterCallback,
myPictureCallback_RAW, myPictureCallback_JPG);
}
}
and some callbacks:
Camera.ShutterCallback myShutterCallback = new Camera.ShutterCallback() {
@Override
public void onShutter() {
}
};
Camera.PictureCallback myPictureCallback_RAW = new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
}
};
Camera.PictureCallback myPictureCallback_JPG = new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
Bitmap bitmapPicture
= BitmapFactory.decodeByteArray(data, 0, data.length);
Bitmap croppedBitmap = null;
Display display = ((WindowManager) context.getSystemService
(Context.WINDOW_SERVICE)).getDefaultDisplay();
if (display.getRotation() == Surface.ROTATION_0) {
Matrix matrix = new Matrix();
matrix.postRotate(90);
Bitmap rotatedBitmap = Bitmap.createBitmap(bitmapPicture, 0, 0,
bitmapPicture.getWidth(), bitmapPicture.getHeight(), matrix, true);
createImageFile(rotatedBitmap);
float koefX = (float) rotatedBitmap.getWidth() / (float) previewLayout.getWidth();
float koefY = (float) rotatedBitmap.getHeight() / previewLayout.getHeight();
int x1 = borderCamera.getLeft();
int y1 = borderCamera.getTop();
int x2 = borderCamera.getWidth();
int y2 = borderCamera.getHeight();
int cropStartX = Math.round(x1 * koefX);
int cropStartY = Math.round(y1 * koefY);
int cropWidthX = Math.round(x2 * koefX);
int cropHeightY = Math.round(y2 * koefY);
if (cropStartX + cropWidthX <= rotatedBitmap.getWidth() &&
cropStartY + cropHeightY <= rotatedBitmap.getHeight()) {
croppedBitmap = Bitmap.createBitmap(rotatedBitmap, cropStartX,
cropStartY, cropWidthX, cropHeightY);
} else {
croppedBitmap = null;
}
if (croppedBitmap != null) {
createImageFile(croppedBitmap);
}
} else if (display.getRotation() == Surface.ROTATION_270) {
}
if (mListener != null) {
if (croppedBitmap != null)
mListener.onFragmentInteraction(croppedBitmap);
}
if (camera != null) {
camera.startPreview();
}
}
};
Calculation to crop image is simple:
float koefX = (float) rotatedBitmap.getWidth() / (float) previewLayout.getWidth();
float koefY = (float) rotatedBitmap.getHeight() / (float)previewLayout.getHeight();
int x1 = borderCamera.getLeft();
int y1 = borderCamera.getTop();
int x2 = borderCamera.getWidth();
int y2 = borderCamera.getHeight();
int cropStartX = Math.round(x1 * koefX);
int cropStartY = Math.round(y1 * koefY);
int cropWidthX = Math.round(x2 * koefX);
int cropHeightY = Math.round(y2 * koefY);
if (cropStartX + cropWidthX <= rotatedBitmap.getWidth() && cropStartY +
cropHeightY <= rotatedBitmap.getHeight()) {
croppedBitmap = Bitmap.createBitmap
(rotatedBitmap, cropStartX, cropStartY, cropWidthX, cropHeightY);
} else {
croppedBitmap = null;
}
Also, we need write bitmap to file:
public void createImageFile(final Bitmap bitmap) {
File path = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES);
String timeStamp = new SimpleDateFormat("MMdd_HHmmssSSS").format(new Date());
String imageFileName = "region_" + timeStamp + ".jpg";
final File file = new File(path, imageFileName);
try {
if (path.mkdirs()) {
Toast.makeText(context, "Not exist :" + path.getName(), Toast.LENGTH_SHORT).show();
}
OutputStream os = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
os.flush();
os.close();
Log.i("ExternalStorage", "Writed " + path + file.getName());
MediaScannerConnection.scanFile(context,
new String[]{file.toString()}, null,
new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
Log.i("ExternalStorage", "Scanned " + path + ":");
Log.i("ExternalStorage", "-> uri=" + uri);
}
});
Toast.makeText(context, file.getName(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.w("ExternalStorage", "Error writing " + file, e);
}
}
Design MainActivity
class:
="1.0"="utf-8"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/make_photo_button"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Photo!" />
</LinearLayout>
<LinearLayout
android:id="@+id/res_photo_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</LinearLayout>
</FrameLayout>
and put some code:
public class MainActivity extends AppCompatActivity
implements PhotoFragment.OnFragmentInteractionListener {
int PERMISSION_ALL = 1;
boolean flagPermissions = false;
String[] PERMISSIONS = {
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
android.Manifest.permission.CAMERA
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
checkPermissions();
}
@OnClick(R.id.make_photo_button)
void onClickScanButton() {
if (!flagPermissions) {
checkPermissions();
return;
}
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.res_photo_layout, new PhotoFragment())
.addToBackStack(null)
.commit();
}
void checkPermissions() {
if (!hasPermissions(this, PERMISSIONS)) {
requestPermissions(PERMISSIONS,
PERMISSION_ALL);
flagPermissions = false;
}
flagPermissions = true;
}
public static boolean hasPermissions(Context context, String... permissions) {
if (context != null && permissions != null) {
for (String permission : permissions) {
if (ActivityCompat.checkSelfPermission(context, permission)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
}
}
return true;
}
@Override
public void onFragmentInteraction(Bitmap bitmap) {
if (bitmap != null) {
ImageFragment imageFragment = new ImageFragment();
imageFragment.imageSetupFragment(bitmap);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.res_photo_layout, imageFragment)
.addToBackStack(null)
.commit();
}
}
}
Test of crop:
Points of Interest
I hope this simple article will help you. You can easily improve this application. I like to develop applications, so you can try some of them on https://play.google.com/store/apps/developer?id=VOLOSHYN+SERGIY.