I was recently tasked with creating a simple image cropper for an Android app we're building using Xamarin. By 'simple' I mean that the resulting image is always of a fixed aspect ratio - in this case, square.
We start off with an imageview:
<imageview nbsp="" p="">android:background="#fff"
android:id="@+id/photoHolder"
android:layout_gravity="center_horizontal"
android:scaletype="matrix"
android:src="@drawable/TapToAddPhoto_Placeholder">
</imageview>
In our activity we handle the touch event like this:
/// handles touch event so we can pick up on drags/zooms etc
public override bool OnTouchEvent(MotionEvent theEvent)
{
if (_hasProvidedPhoto)
{
// ReSharper disable once BitwiseOperatorOnEnumWithoutFlags
switch (theEvent.Action & MotionEventActions.Mask)
{
case MotionEventActions.Down:
_savedMatrix.Set(_matrix);
_start.Set(theEvent.GetX(), theEvent.GetY());
_mode = Drag;
break;
case MotionEventActions.PointerDown:
_oldDist = Spacing(theEvent);
if (_oldDist > 10f)
{
_savedMatrix.Set(_matrix);
MidPoint(_mid, theEvent);
_mode = Zoom;
}
break;
case MotionEventActions.Up:
case MotionEventActions.PointerUp:
_mode = None;
break;
case MotionEventActions.Move:
if (_mode == Drag)
{
_matrix.Set(_savedMatrix);
_matrix.PostTranslate(theEvent.GetX() - _start.X, theEvent.GetY()
- _start.Y);
}
else if (_mode == Zoom)
{
float newDist = Spacing(theEvent);
if (newDist > 10f)
{
_matrix.Set(_savedMatrix);
float scale = newDist / _oldDist;
_matrix.PostScale(scale, scale, _mid.X, _mid.Y);
}
}
break;
}
_photoHolder.ImageMatrix = _matrix;
}
return true;
}
//Determine the space between the first two fingers
private static float Spacing(MotionEvent theEvent)
{
float x = theEvent.GetX(0) - theEvent.GetX(1);
float y = theEvent.GetY(0) - theEvent.GetY(1);
return FloatMath.Sqrt(x * x + y * y);
}
//Calculate the mid point of the first two fingers
private static void MidPoint(PointF point, MotionEvent theEvent)
{
float x = theEvent.GetX(0) + theEvent.GetX(1);
float y = theEvent.GetY(0) + theEvent.GetY(1);
point.Set(x / 2, y / 2);
}
Where _photoHolder is our imageview declared in OnCreate with _photoHolder = FindViewById
(Resource.Id.photoHolder).
Then, when the user is happy with their crop, they click a button that creates a bitmap of it with this code:
var bitmap = Bitmap.CreateBitmap(v.Width, v.Height, Bitmap.Config.Argb8888);
var canvas = new Canvas(bitmap);
v.Draw(canvas);
return bitmap;
Where v is the ImageView.
This is basically the guts of it. The whole journey of selecting an existing image from the gallery or camera, processing it and dropping it into the image view in the first place proved more tricky than the cropping part.
Here we ask the user if they want to use gallery or camera:
void addPhotoButton_Click(object sender, EventArgs e)
{
string[] options = { "Choose from library", "Take a photo" };
var builder = new AlertDialog.Builder(this);
builder.SetCancelable(true);
builder.SetPositiveButton("Cancel", delegate
{
//nothing. just close
});
builder.SetItems(options, PhotoChoiceDialogSelected);
builder.Show();
}
And here we handle the choice:
private void PhotoChoiceDialogSelected(object sender, DialogClickEventArgs e)
{
if (e.Which == 0) //choose from library
{
GetPhotoFromLibrary();
}
else //take photo
{
GetPhotoFromCamera();
}
}
Here are our methods for camera or gallery options:
private void GetPhotoFromLibrary()
{
var imageIntent = new Intent();
imageIntent.SetType("image/*");
imageIntent.SetAction(Intent.ActionGetContent);
StartActivityForResult(
Intent.CreateChooser(imageIntent, "Select photo"), 0);
}
private void GetPhotoFromCamera()
{
if (IsThereAnAppToTakePictures())
{
CreateDirectoryForPictures();
var intent = new Intent(MediaStore.ActionImageCapture);
CameraFile = new File(CameraDir, String.Format("myPhoto_{0}.jpg", Guid.NewGuid()));
intent.PutExtra(MediaStore.ExtraOutput, Android.Net.Uri.FromFile(CameraFile));
StartActivityForResult(intent, 1);
}
else
{
Toast.MakeText(this, "You do not have a camera", ToastLength.Short).Show();
}
}
private void CreateDirectoryForPictures()
{
CameraDir = new File(Environment.GetExternalStoragePublicDirectory(Environment.DirectoryPictures), "My App Pics");
if (!CameraDir.Exists())
{
CameraDir.Mkdirs();
}
}
private bool IsThereAnAppToTakePictures()
{
var intent = new Intent(MediaStore.ActionImageCapture);
IList availableActivities = PackageManager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly);
return availableActivities != null && availableActivities.Count > 0;
}
And this is what runs when the camera or gallery activity has finished:
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (resultCode == Result.Ok)
{
var camera = false;
if (requestCode == 1)
{
camera = true;
// make camera image available in the gallery
var mediaScanIntent = new Intent(Intent.ActionMediaScannerScanFile);
_babyImageUri = Android.Net.Uri.FromFile(CameraFile);
mediaScanIntent.SetData(_babyImageUri);
SendBroadcast(mediaScanIntent);
}
_filePath = camera ? CameraFile.AbsolutePath : GetPathToGalleryImage(data.Data);
_fileUri = camera ? _babyImageUri : data.Data;
if (_filePath != null)
{
var scaledBitmap = DecodeSampledBitmapFromResource(
_filePath, 575, 575);
if (scaledBitmap != null)
{
var matrix1 = new Matrix();
matrix1.PostRotate(GetCorrectOrientation(_filePath));
var rotatedBitmap = Bitmap.CreateBitmap(scaledBitmap, 0, 0, scaledBitmap.Width,
scaledBitmap.Height,
matrix1, true);
_photoHolder.SetImageBitmap(rotatedBitmap);
_photoHolder.LayoutParameters.Height = _photoHolder.Width;
}
}
}
}
And these are some important supporting methods used in the above code:
///
/// takes a uri to a gallery image and returns a path
///
///
///
private string GetPathToGalleryImage(Android.Net.Uri uri)
{
if (uri != null)
{
string path = null;
// The projection contains the columns we want to return in our query.
var projection = new[] { MediaStore.Images.Media.InterfaceConsts.Data };
using (ICursor cursor = ManagedQuery(uri, projection, null, null, null))
{
if (cursor != null)
{
int columnIndex = cursor.GetColumnIndexOrThrow(MediaStore.Images.Media.InterfaceConsts.Data);
cursor.MoveToFirst();
path = cursor.GetString(columnIndex);
}
}
return path;
}
return null;
}
///
/// google recommended code for displaying large bitmaps
///
///
///
///
///
public Bitmap DecodeSampledBitmapFromResource(String path,
int reqWidth, int reqHeight)
{
// First decode with inJustDecodeBounds=true to check dimensions
var options = new BitmapFactory.Options
{
InJustDecodeBounds = true,
InPreferredConfig = Bitmap.Config.Argb8888
};
BitmapFactory.DecodeFile(path, options);
// Calculate inSampleSize
options.InSampleSize = CalculateInSampleSize(options, reqWidth,
reqHeight);
// Decode bitmap with inSampleSize set
options.InJustDecodeBounds = false;
return BitmapFactory.DecodeFile(path, options);
}
///
/// google recommended code for displaying large bitmaps
///
///
///
///
///
public int CalculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight)
{
// Raw height and width of image
int height = options.OutHeight;
int width = options.OutWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth)
{
int halfHeight = height / 2;
int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2
// and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth)
{
inSampleSize *= 2;
}
}
return inSampleSize;
}
private float GetCorrectOrientation(String filename)
{
ExifInterface exif = new ExifInterface(filename);
int orientation = exif.GetAttributeInt(ExifInterface.TagOrientation, 1);
switch (orientation)
{
case 6:
return 90;
case 3:
return 180;
case 8:
return 270;
default:
return 0;
}
}