GitHub: uCrop, 版本为 2.2.2
主要是探究一下内部对于图片按比例的裁剪以及压缩, 应该会更很长一段时间
疑惑点
这里记下一些源码分析过程中遇到的疑惑点
sample/src/main/res/layout/activity_sample.xml 内
Toolbar
是 GONE 的,FrameLayout
宽度高度都是 0,既然都不用,留着做什么?sample/src/main/res/layout/activity_sample.xml 内 include 引入的布局定义了
id
属性有什么用?sample app/build.gradle 中定义两个 flavors(activity, fragment) 用来做什么?
整个项目结构
clone 下来的源代码包含两个 module, sample 和 ucrop, simple 是一个可运行的 module, ucrop 是其依赖的库
sample module
这个 module 可以看到 ucrop 的一些用法
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yalantis.ucrop.sample">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="@string/file_provider_authorities"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>
<activity
android:name=".SampleActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ResultActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
</application>
</manifest>
先看 AndroidManifest.xml 文件, 注意 application
节点的 android:allowBackup="false"
属性,android:allowBackup
官方解释为
Whether to allow the application to participate in the backup and restore infrastructure. If this attribute is set to false, no backup or restore of the application will ever be performed, even by a full-system backup that would otherwise cause all application data to be saved via adb. The default value of this attribute is true
该属性指示该软件是否可以备份,为 true
的话可能会带来一些安全问题,比如备份该软件后恢复在其他设备上,会造成数据泄漏与转移;接下来声明了一个内容提供器;最后声明了三个 Activity
启动 Activity 为 SampleActivity
, 代码转到 SampleActivity.java. 其继承自 BaseActivity
并实现了 UCropFragmentCallback
接口.
UCropFragmentCallback.java
public interface UCropFragmentCallback {
/**
* Return loader status
* @param showLoader
*/
void loadingProgress(boolean showLoader);
/**
* Return cropping result or error
* @param result
*/
void onCropFinish(UCropFragment.UCropResult result);
}
先看 UCropFragmentCallback
接口, 这个接口的定义是在 ucrop 模块中, 内部只定义了两个方法, void loadingProgress(boolean showLoader)
与 void onCropFinish(UCropFragment.UCropResult result)
, 注释说明前者返回启动器状态, 后者返回裁剪的结果或者错误.
再看看 BaseActivity
, 其继承自 AppCompatActivity
, 正常操作
BaseActivity.java
public class BaseActivity extends AppCompatActivity {
protected static final int REQUEST_STORAGE_READ_ACCESS_PERMISSION = 101;
protected static final int REQUEST_STORAGE_WRITE_ACCESS_PERMISSION = 102;
private AlertDialog mAlertDialog;
/**
* Hide alert dialog if any.
*/
@Override
protected void onStop() {
super.onStop();
if (mAlertDialog != null && mAlertDialog.isShowing()) {
mAlertDialog.dismiss();
}
}
/**
* Requests given permission.
* If the permission has been denied previously, a Dialog will prompt the user to grant the
* permission, otherwise it is requested directly.
*/
protected void requestPermission(final String permission, String rationale, final int requestCode) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
showAlertDialog(getString(R.string.permission_title_rationale), rationale,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(BaseActivity.this,
new String[]{permission}, requestCode);
}
}, getString(R.string.label_ok), null, getString(R.string.label_cancel));
} else {
ActivityCompat.requestPermissions(this, new String[]{permission}, requestCode);
}
}
/**
* This method shows dialog with given title & message.
* Also there is an option to pass onClickListener for positive & negative button.
*
* @param title - dialog title
* @param message - dialog message
* @param onPositiveButtonClickListener - listener for positive button
* @param positiveText - positive button text
* @param onNegativeButtonClickListener - listener for negative button
* @param negativeText - negative button text
*/
protected void showAlertDialog(@Nullable String title, @Nullable String message,
@Nullable DialogInterface.OnClickListener onPositiveButtonClickListener,
@NonNull String positiveText,
@Nullable DialogInterface.OnClickListener onNegativeButtonClickListener,
@NonNull String negativeText) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(title);
builder.setMessage(message);
builder.setPositiveButton(positiveText, onPositiveButtonClickListener);
builder.setNegativeButton(negativeText, onNegativeButtonClickListener);
mAlertDialog = builder.show();
}
}
提供了 protected void requestPermission(final String permission, String rationale, final int requestCode)
与 protected void showAlertDialog(@Nullable String title, @Nullable String message, @Nullable DialogInterface.OnClickListener onPositiveButtonClickListener, @NonNull String positiveText, @Nullable DialogInterface.OnClickListener onNegativeButtonClickListener, @NonNull String negativeText)
方法的定义
requestPermission 方法内部第一行 ActivityCompat.shouldShowRequestPermissionRationale(this, permission)
, sboolean shouldShowRequestPermissionRationale (Activity activity, String permission)
文档中的解释是
Gets whether you should show UI with rationale for requesting a permission. You should do this only if you do not have the permission and the context in which the permission is requested does not clearly communicate to the user what would be the benefit from granting this permission.
大概意思就是返回是否应该显示授予权限时的提示框, 文档说的不是很清楚, 该方法会在用户第一次拒绝授予权限后再次申请时返回 true
, 但是如果用户选择了"以后不再询问", 则会返回 false
, 根据该方法的返回值决定是弹窗 AlertDialog
告诉用户为什么需要该权限还是不弹窗直接申请该权限, 当然, AlertDialog
的确定按钮点击事件还是直接申请权限, showAlertDialog
方法很正常, 单纯的构造 AlertDialog
并保存其引用, 没有什么可说的
最后是它的覆盖方法 protected void onStop()
, 该方法会在该 Activity 停止之前将 AlertDialog
隐藏掉, 算是一个奇淫技巧吧, 但是想不出什么情况下有可能该 Activity 已经停止但是 AlertDialog
还会继续显示
SampleActivity.java
public class SampleActivity extends BaseActivity implements UCropFragmentCallback {
private static final String TAG = "SampleActivity";
private static final int REQUEST_SELECT_PICTURE = 0x01;
private static final int REQUEST_SELECT_PICTURE_FOR_FRAGMENT = 0x02;
private static final String SAMPLE_CROPPED_IMAGE_NAME = "SampleCropImage";
private RadioGroup mRadioGroupAspectRatio, mRadioGroupCompressionSettings;
private EditText mEditTextMaxWidth, mEditTextMaxHeight;
private EditText mEditTextRatioX, mEditTextRatioY;
private CheckBox mCheckBoxMaxSize;
private SeekBar mSeekBarQuality;
private TextView mTextViewQuality;
private CheckBox mCheckBoxHideBottomControls;
private CheckBox mCheckBoxFreeStyleCrop;
private Toolbar toolbar;
private ScrollView settingsView;
private int requestMode = BuildConfig.RequestMode;
private UCropFragment fragment;
private boolean mShowLoader;
private String mToolbarTitle;
@DrawableRes
private int mToolbarCancelDrawable;
@DrawableRes
private int mToolbarCropDrawable;
// Enables dynamic coloring
private int mToolbarColor;
private int mStatusBarColor;
private int mToolbarWidgetColor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sample);
setupUI();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
if (requestCode == requestMode) {
final Uri selectedUri = data.getData();
if (selectedUri != null) {
startCrop(selectedUri);
} else {
Toast.makeText(SampleActivity.this, R.string.toast_cannot_retrieve_selected_image, Toast.LENGTH_SHORT).show();
}
} else if (requestCode == UCrop.REQUEST_CROP) {
handleCropResult(data);
}
}
if (resultCode == UCrop.RESULT_ERROR) {
handleCropError(data);
}
}
/**
* Callback received when a permissions request has been completed.
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_STORAGE_READ_ACCESS_PERMISSION:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
pickFromGallery();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
@SuppressWarnings("ConstantConditions")
private void setupUI() {
findViewById(R.id.button_crop).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pickFromGallery();
}
});
findViewById(R.id.button_random_image).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Random random = new Random();
int minSizePixels = 800;
int maxSizePixels = 2400;
Uri uri = Uri.parse(String.format(Locale.getDefault(), "https://unsplash.it/%d/%d/?random",
minSizePixels + random.nextInt(maxSizePixels - minSizePixels),
minSizePixels + random.nextInt(maxSizePixels - minSizePixels)));
startCrop(uri);
}
});
settingsView = findViewById(R.id.settings);
mRadioGroupAspectRatio = findViewById(R.id.radio_group_aspect_ratio);
mRadioGroupCompressionSettings = findViewById(R.id.radio_group_compression_settings);
mCheckBoxMaxSize = findViewById(R.id.checkbox_max_size);
mEditTextRatioX = findViewById(R.id.edit_text_ratio_x);
mEditTextRatioY = findViewById(R.id.edit_text_ratio_y);
mEditTextMaxWidth = findViewById(R.id.edit_text_max_width);
mEditTextMaxHeight = findViewById(R.id.edit_text_max_height);
mSeekBarQuality = findViewById(R.id.seekbar_quality);
mTextViewQuality = findViewById(R.id.text_view_quality);
mCheckBoxHideBottomControls = findViewById(R.id.checkbox_hide_bottom_controls);
mCheckBoxFreeStyleCrop = findViewById(R.id.checkbox_freestyle_crop);
mRadioGroupAspectRatio.check(R.id.radio_dynamic);
mEditTextRatioX.addTextChangedListener(mAspectRatioTextWatcher);
mEditTextRatioY.addTextChangedListener(mAspectRatioTextWatcher);
mRadioGroupCompressionSettings.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
mSeekBarQuality.setEnabled(checkedId == R.id.radio_jpeg);
}
});
mRadioGroupCompressionSettings.check(R.id.radio_jpeg);
mSeekBarQuality.setProgress(UCropActivity.DEFAULT_COMPRESS_QUALITY);
mTextViewQuality.setText(String.format(getString(R.string.format_quality_d), mSeekBarQuality.getProgress()));
mSeekBarQuality.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
mTextViewQuality.setText(String.format(getString(R.string.format_quality_d), progress));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
mEditTextMaxHeight.addTextChangedListener(mMaxSizeTextWatcher);
mEditTextMaxWidth.addTextChangedListener(mMaxSizeTextWatcher);
}
private TextWatcher mAspectRatioTextWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
mRadioGroupAspectRatio.clearCheck();
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
}
};
private TextWatcher mMaxSizeTextWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (s != null && !s.toString().trim().isEmpty()) {
if (Integer.valueOf(s.toString()) < UCrop.MIN_SIZE) {
Toast.makeText(SampleActivity.this, String.format(getString(R.string.format_max_cropped_image_size), UCrop.MIN_SIZE), Toast.LENGTH_SHORT).show();
}
}
}
};
private void pickFromGallery() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
&& ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
requestPermission(Manifest.permission.READ_EXTERNAL_STORAGE,
getString(R.string.permission_read_storage_rationale),
REQUEST_STORAGE_READ_ACCESS_PERMISSION);
} else {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT)
.setType("image/*")
.addCategory(Intent.CATEGORY_OPENABLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
String[] mimeTypes = {"image/jpeg", "image/png"};
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
}
startActivityForResult(Intent.createChooser(intent, getString(R.string.label_select_picture)), requestMode);
}
}
private void startCrop(@NonNull Uri uri) {
String destinationFileName = SAMPLE_CROPPED_IMAGE_NAME;
switch (mRadioGroupCompressionSettings.getCheckedRadioButtonId()) {
case R.id.radio_png:
destinationFileName += ".png";
break;
case R.id.radio_jpeg:
destinationFileName += ".jpg";
break;
}
UCrop uCrop = UCrop.of(uri, Uri.fromFile(new File(getCacheDir(), destinationFileName)));
uCrop = basisConfig(uCrop);
uCrop = advancedConfig(uCrop);
if (requestMode == REQUEST_SELECT_PICTURE_FOR_FRAGMENT) { //if build variant = fragment
setupFragment(uCrop);
} else { // else start uCrop Activity
uCrop.start(SampleActivity.this);
}
}
/**
* In most cases you need only to set crop aspect ration and max size for resulting image.
*
* @param uCrop - ucrop builder instance
* @return - ucrop builder instance
*/
private UCrop basisConfig(@NonNull UCrop uCrop) {
switch (mRadioGroupAspectRatio.getCheckedRadioButtonId()) {
case R.id.radio_origin:
uCrop = uCrop.useSourceImageAspectRatio();
break;
case R.id.radio_square:
uCrop = uCrop.withAspectRatio(1, 1);
break;
case R.id.radio_dynamic:
// do nothing
break;
default:
try {
float ratioX = Float.valueOf(mEditTextRatioX.getText().toString().trim());
float ratioY = Float.valueOf(mEditTextRatioY.getText().toString().trim());
if (ratioX > 0 && ratioY > 0) {
uCrop = uCrop.withAspectRatio(ratioX, ratioY);
}
} catch (NumberFormatException e) {
Log.i(TAG, String.format("Number please: %s", e.getMessage()));
}
break;
}
if (mCheckBoxMaxSize.isChecked()) {
try {
int maxWidth = Integer.valueOf(mEditTextMaxWidth.getText().toString().trim());
int maxHeight = Integer.valueOf(mEditTextMaxHeight.getText().toString().trim());
if (maxWidth > UCrop.MIN_SIZE && maxHeight > UCrop.MIN_SIZE) {
uCrop = uCrop.withMaxResultSize(maxWidth, maxHeight);
}
} catch (NumberFormatException e) {
Log.e(TAG, "Number please", e);
}
}
return uCrop;
}
/**
* Sometimes you want to adjust more options, it's done via {@link com.yalantis.ucrop.UCrop.Options} class.
*
* @param uCrop - ucrop builder instance
* @return - ucrop builder instance
*/
private UCrop advancedConfig(@NonNull UCrop uCrop) {
UCrop.Options options = new UCrop.Options();
switch (mRadioGroupCompressionSettings.getCheckedRadioButtonId()) {
case R.id.radio_png:
options.setCompressionFormat(Bitmap.CompressFormat.PNG);
break;
case R.id.radio_jpeg:
default:
options.setCompressionFormat(Bitmap.CompressFormat.JPEG);
break;
}
options.setCompressionQuality(mSeekBarQuality.getProgress());
options.setHideBottomControls(mCheckBoxHideBottomControls.isChecked());
options.setFreeStyleCropEnabled(mCheckBoxFreeStyleCrop.isChecked());
/*
If you want to configure how gestures work for all UCropActivity tabs
options.setAllowedGestures(UCropActivity.SCALE, UCropActivity.ROTATE, UCropActivity.ALL);
* */
/*
This sets max size for bitmap that will be decoded from source Uri.
More size - more memory allocation, default implementation uses screen diagonal.
options.setMaxBitmapSize(640);
* */
/*
Tune everything (ノ◕ヮ◕)ノ*:・゚✧
options.setMaxScaleMultiplier(5);
options.setImageToCropBoundsAnimDuration(666);
options.setDimmedLayerColor(Color.CYAN);
options.setCircleDimmedLayer(true);
options.setShowCropFrame(false);
options.setCropGridStrokeWidth(20);
options.setCropGridColor(Color.GREEN);
options.setCropGridColumnCount(2);
options.setCropGridRowCount(1);
options.setToolbarCropDrawable(R.drawable.your_crop_icon);
options.setToolbarCancelDrawable(R.drawable.your_cancel_icon);
// Color palette
options.setToolbarColor(ContextCompat.getColor(this, R.color.your_color_res));
options.setStatusBarColor(ContextCompat.getColor(this, R.color.your_color_res));
options.setActiveWidgetColor(ContextCompat.getColor(this, R.color.your_color_res));
options.setToolbarWidgetColor(ContextCompat.getColor(this, R.color.your_color_res));
options.setRootViewBackgroundColor(ContextCompat.getColor(this, R.color.your_color_res));
// Aspect ratio options
options.setAspectRatioOptions(1,
new AspectRatio("WOW", 1, 2),
new AspectRatio("MUCH", 3, 4),
new AspectRatio("RATIO", CropImageView.DEFAULT_ASPECT_RATIO, CropImageView.DEFAULT_ASPECT_RATIO),
new AspectRatio("SO", 16, 9),
new AspectRatio("ASPECT", 1, 1));
*/
return uCrop.withOptions(options);
}
private void handleCropResult(@NonNull Intent result) {
final Uri resultUri = UCrop.getOutput(result);
if (resultUri != null) {
ResultActivity.startWithUri(SampleActivity.this, resultUri);
} else {
Toast.makeText(SampleActivity.this, R.string.toast_cannot_retrieve_cropped_image, Toast.LENGTH_SHORT).show();
}
}
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
private void handleCropError(@NonNull Intent result) {
final Throwable cropError = UCrop.getError(result);
if (cropError != null) {
Log.e(TAG, "handleCropError: ", cropError);
Toast.makeText(SampleActivity.this, cropError.getMessage(), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(SampleActivity.this, R.string.toast_unexpected_error, Toast.LENGTH_SHORT).show();
}
}
@Override
public void loadingProgress(boolean showLoader) {
mShowLoader = showLoader;
supportInvalidateOptionsMenu();
}
@Override
public void onCropFinish(UCropFragment.UCropResult result) {
switch (result.mResultCode) {
case RESULT_OK:
handleCropResult(result.mResultData);
break;
case UCrop.RESULT_ERROR:
handleCropError(result.mResultData);
break;
}
removeFragmentFromScreen();
}
public void removeFragmentFromScreen() {
getSupportFragmentManager().beginTransaction()
.remove(fragment)
.commit();
toolbar.setVisibility(View.GONE);
settingsView.setVisibility(View.VISIBLE);
}
public void setupFragment(UCrop uCrop) {
fragment = uCrop.getFragment(uCrop.getIntent(this).getExtras());
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, fragment, UCropFragment.TAG)
.commitAllowingStateLoss();
setupViews(uCrop.getIntent(this).getExtras());
}
public void setupViews(Bundle args) {
settingsView.setVisibility(View.GONE);
mStatusBarColor = args.getInt(UCrop.Options.EXTRA_STATUS_BAR_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_statusbar));
mToolbarColor = args.getInt(UCrop.Options.EXTRA_TOOL_BAR_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_toolbar));
mToolbarCancelDrawable = args.getInt(UCrop.Options.EXTRA_UCROP_WIDGET_CANCEL_DRAWABLE, R.drawable.ucrop_ic_cross);
mToolbarCropDrawable = args.getInt(UCrop.Options.EXTRA_UCROP_WIDGET_CROP_DRAWABLE, R.drawable.ucrop_ic_done);
mToolbarWidgetColor = args.getInt(UCrop.Options.EXTRA_UCROP_WIDGET_COLOR_TOOLBAR, ContextCompat.getColor(this, R.color.ucrop_color_toolbar_widget));
mToolbarTitle = args.getString(UCrop.Options.EXTRA_UCROP_TITLE_TEXT_TOOLBAR);
mToolbarTitle = mToolbarTitle != null ? mToolbarTitle : getResources().getString(R.string.ucrop_label_edit_photo);
setupAppBar();
}
/**
* Configures and styles both status bar and toolbar.
*/
private void setupAppBar() {
setStatusBarColor(mStatusBarColor);
toolbar = findViewById(R.id.toolbar);
// Set all of the Toolbar coloring
toolbar.setBackgroundColor(mToolbarColor);
toolbar.setTitleTextColor(mToolbarWidgetColor);
toolbar.setVisibility(View.VISIBLE);
final TextView toolbarTitle = toolbar.findViewById(R.id.toolbar_title);
toolbarTitle.setTextColor(mToolbarWidgetColor);
toolbarTitle.setText(mToolbarTitle);
// Color buttons inside the Toolbar
Drawable stateButtonDrawable = ContextCompat.getDrawable(getBaseContext(), mToolbarCancelDrawable);
if (stateButtonDrawable != null) {
stateButtonDrawable.mutate();
stateButtonDrawable.setColorFilter(mToolbarWidgetColor, PorterDuff.Mode.SRC_ATOP);
toolbar.setNavigationIcon(stateButtonDrawable);
}
setSupportActionBar(toolbar);
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(false);
}
}
/**
* Sets status-bar color for L devices.
*
* @param color - status-bar color
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void setStatusBarColor(@ColorInt int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
final Window window = getWindow();
if (window != null) {
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(color);
}
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.ucrop_menu_activity, menu);
// Change crop & loader menu icons color to match the rest of the UI colors
MenuItem menuItemLoader = menu.findItem(R.id.menu_loader);
Drawable menuItemLoaderIcon = menuItemLoader.getIcon();
if (menuItemLoaderIcon != null) {
try {
menuItemLoaderIcon.mutate();
menuItemLoaderIcon.setColorFilter(mToolbarWidgetColor, PorterDuff.Mode.SRC_ATOP);
menuItemLoader.setIcon(menuItemLoaderIcon);
} catch (IllegalStateException e) {
Log.i(this.getClass().getName(), String.format("%s - %s", e.getMessage(), getString(R.string.ucrop_mutate_exception_hint)));
}
((Animatable) menuItemLoader.getIcon()).start();
}
MenuItem menuItemCrop = menu.findItem(R.id.menu_crop);
Drawable menuItemCropIcon = ContextCompat.getDrawable(this, mToolbarCropDrawable);
if (menuItemCropIcon != null) {
menuItemCropIcon.mutate();
menuItemCropIcon.setColorFilter(mToolbarWidgetColor, PorterDuff.Mode.SRC_ATOP);
menuItemCrop.setIcon(menuItemCropIcon);
}
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.menu_crop).setVisible(!mShowLoader);
menu.findItem(R.id.menu_loader).setVisible(mShowLoader);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_crop) {
if (fragment.isAdded())
fragment.cropAndSaveImage();
} else if (item.getItemId() == android.R.id.home) {
removeFragmentFromScreen();
}
return super.onOptionsItemSelected(item);
}
}
按照方法调用顺序,先从 protected void onCreate(Bundle savedInstanceState)
看起,主要就是两个方法,setContentView(R.layout.activity_sample)
设置静态布局和 setUI()
。
先看 R.layout.activity_sample
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:visibility="gone">
<TextView
android:id="@+id/toolbar_title"
style="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</android.support.v7.widget.Toolbar>
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
<include
android:id="@+id/settings"
layout="@layout/include_settings" />
</android.support.constraint.ConstraintLayout>
这里代码很简单,根元素 ConstraintLayout
下首先是定义了一个隐藏(区别于不可见) Toolbar
,注意此处的 android:minHeight="?attr/actionBarSize"
设置了最小高度,内部包含另一个 TextView
控件 style="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
,应该是用来做 Toolbar 的标题栏;下来是一个 FrameLayout
,位于 Toolbar
下方,ID 为 fragment_container
,应该是用来容纳 Fragment 的,但是宽度高度都是 0,这里暂时不明白是为什么;接下来是一个 include 标签引入了 @layout/include_settings
这个布局,代码转到 include_settings.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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"
android:background="@android:color/white"
android:fillViewport="true">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true">
<LinearLayout
android:id="@+id/wrapper_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:background="@drawable/bg_rounded_rectangle"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/app_name"
android:textColor="@color/colorAccent"
android:textSize="42sp"
android:textStyle="bold" />
<Button
android:id="@+id/button_crop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/button_pick_amp_crop"
android:textAllCaps="true"
android:textAppearance="?android:textAppearanceMedium"
android:textColor="@android:color/white"
android:textStyle="bold" />
<Button
android:id="@+id/button_random_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/button_crop_random_image"
android:textAllCaps="true"
android:textAppearance="?android:textAppearanceMedium"
android:textColor="@android:color/white"
android:textStyle="bold" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="5dp"
android:background="@color/colorAccent" />
<RadioGroup
android:id="@+id/radio_group_aspect_ratio"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/label_aspect_ratio"
android:textAppearance="?android:textAppearanceSmall" />
<CheckBox
android:id="@+id/checkbox_freestyle_crop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/label_freestyle_crop"
android:textAppearance="?android:textAppearanceMedium" />
<RadioButton
android:id="@+id/radio_dynamic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/label_dynamic"
android:textAppearance="?android:textAppearanceMedium"
tools:checked="true" />
<RadioButton
android:id="@+id/radio_origin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/label_image_source"
android:textAppearance="?android:textAppearanceMedium" />
<RadioButton
android:id="@+id/radio_square"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/label_square"
android:textAppearance="?android:textAppearanceMedium" />
<LinearLayout
android:layout_width="140dp"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/edit_text_ratio_x"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:hint="x"
android:inputType="numberDecimal" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="to"
tools:ignore="HardcodedText" />
<EditText
android:id="@+id/edit_text_ratio_y"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:hint="y"
android:inputType="numberDecimal" />
</LinearLayout>
</RadioGroup>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="5dp"
android:background="@color/colorAccent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/label_max_cropped_image_size"
android:textAppearance="?android:textAppearanceSmall" />
<CheckBox
android:id="@+id/checkbox_max_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/label_resize_image_to_max_size"
android:textAppearance="?android:textAppearanceMedium" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/edit_text_max_width"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:hint="@string/label_width"
android:inputType="number" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="x"
tools:ignore="HardcodedText" />
<EditText
android:id="@+id/edit_text_max_height"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:hint="@string/label_height"
android:inputType="number" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="5dp"
android:background="@color/colorAccent" />
<RadioGroup
android:id="@+id/radio_group_compression_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/label_compression_settings"
android:textAppearance="?android:textAppearanceSmall" />
<RadioButton
android:id="@+id/radio_jpeg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="JPEG"
android:textAppearance="?android:textAppearanceMedium"
tools:checked="true"
tools:ignore="HardcodedText" />
<RadioButton
android:id="@+id/radio_png"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="PNG"
android:textAppearance="?android:textAppearanceMedium"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/text_view_quality"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textAppearance="?android:textAppearanceSmall"
tools:text="Quality: 90" />
<SeekBar
android:id="@+id/seekbar_quality"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:progress="90" />
</RadioGroup>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="5dp"
android:background="@color/colorAccent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/label_ui"
android:textAppearance="?android:textAppearanceSmall" />
<CheckBox
android:id="@+id/checkbox_hide_bottom_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/label_hide_bottom_ui_controls"
android:textAppearance="?android:textAppearanceMedium" />
</LinearLayout>
</FrameLayout>
</ScrollView>
根元素为一个 ScrollView
,允许内容超出屏幕时滚动,其有一个 android:fillViewport="true"
属性,官方解释:
Defines whether the scrollview should stretch its content to fill the viewport.
解析的挺清晰,将 ScrollView
高度扩展到整个父布局,如果没有设置这个即使宽高都是 match_parent
,Scroller 的宽高最大值也只是内部元素的宽高,此时 layout_gravity
表现(尤其是 bottom
)将会和预期不一致;ScrollView
子元素为 FrameLayout
,这里有两个属性需要注意一下,android:focusable="true"
,android:focusableInTouchMode="true"
给 FrameLayout
获得焦点做什么?实际测试中如果不加 android:focusableInTouchMode="true"
这个属性,那么打开应用的时候焦点会自动在下方的 EditText
中,官方文档中 android:focusable
解释为
Controls whether a view can take focus. By default, this is "auto" which lets the framework determine whether a user can move focus to a view. By setting this attribute to true the view is allowed to take focus. By setting it to "false" the view will not take focus. This value does not impact the behavior of directly calling View.requestFocus(), which will always request focus regardless of this view. It only impacts where focus navigation will try to move focus.
android:focusableInTouchMode
解释为
Boolean that controls whether a view can take focus while in touch mode. If this is true for a view, that view can gain focus when clicked on, and can keep focus if another view is clicked on that doesn't have this attribute set to true.
这文档着实划水,不过这里有一篇不错的 博文 讲的比较清楚,总结一下就是 android:focusable
允许软件层面(如遥控器)的非触摸聚焦,而 android:focusableInTouchMode
允许硬件层面(触屏)的触控聚焦,最重要的一点是有控件如 EditText
会自动获得焦点,而 android:focusableInTouchMode=true
则会改变子控件的这一行为,由于触控事件是由父 View
向子 View
传递,父 View
设置这一属性即可实现“拦截子控件自动获取焦点”。
继续往下,子 View
是一个 LinerLayout
,其 android:layout_marginBottom="@dimen/activity_vertical_margin"
属性值的定义方式是引用 dimen
,可以看到有一个 values/dimens.xml 文件,其中i定义了几个 dimen 用来表示一些空间及内容大小方面的值,然后用不同的设备配置限定符就可以做到适配
<resources>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>
还有它的 android:background="@drawable/bg_rounded_rectangle"
,drawable/bg_rounded_rectangle 是一个 xml 文件,内容如下
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners
android:bottomLeftRadius="3dp"
android:bottomRightRadius="3dp"
android:radius="1dp"
android:topLeftRadius="3dp"
android:topRightRadius="3dp"/>
<stroke
android:width="1dp"
android:color="@color/colorAccent"/>
<solid android:color="@android:color/transparent"/>
</shape>
这是软件启动后最外层的那个橘色的框,关于 shape
平时用的比较少,具体可以看这篇 博文,上面的 shape 就是定义了一个指定描边(stroke)宽度与颜色,内部填色(solid)圆角(corners)矩形(shape),使用 shape 做 background 真的是好看,清晰又方便
再下面又是一个骚操作,TextView
作为应用的标题,尤其是加上 android:textStyle="bold"
属性加上之后,毫无违和感
没错,下一个 Button
里又有骚操作,android:textAppearance="?android:textAppearanceMedium"
用于设置外观,接受另一个资源的引用或主题属性的引用
再下面的一个 Button
没什么好说的,接下来用 View
做了一个分割线,接下来是 RadioGroup
做选择框,值得注意的是除了 RadioButton
外 RadioGroup
还可以含有其他元素,而 RadioButton
的 tools:checked="true"
属性设置其再预览视图中可以看到该选项被选中,为什么这么做?就为看看?后来发现再其对应的 Activity
中设置界面时,该 RadioButton
同样是默认选中的,这就保持了预览图与实际运行的一致性;接下来几个元素波澜不惊,直到下一个 TextView
,它有一个 tools:ignore="HardcodedText"
属性,tools:ignore
属性告诉 Lint 忽略某些提醒,而 HardcodedText
则是忽略 XML 代码中对于字符串的硬编码;继续向下,没什么好说的,直到 SeekBar
,这是一个可拖动的进度条控件,这段代码里都是正常的使用,关于 SeekBar
的问题,这篇 博文 写的不错。还有就是要善于使用 CheckBox
,虽然自己用的都很丑;打开布局即时预览工具,惊讶的发现他的布局预览并没有 ActionBar,而且其 Toolbar 是 GONE 的,首先考虑是主题的原因,查看 Manifest 文件发现主题是默认的 @style/AppTheme
,但是其并没有 styles.xml 文件,反倒有一个 themes.xml
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="colorButtonNormal">@color/colorAccent</item>
</style>
</resources>
原来 style
的定义不一定要写在 style.xml
里也能以 @style/...
的方式引用,其继承自 Theme.AppCompat.Light.NoActionBar
,所以没有 ActionBar,还有 colorButtonNormal
可以全局定义 Button 的背景色,style 与 theme 的区别在于 theme 是针对窗体级别的,而 style 是针对窗体元素的,也就是说 theme 里可能会含有许多 style,具体可以看看这篇 博文
好了,include_settings.xml 算是分析完了,接下来回到 SampleActivity.java
上面看完了 setContentView(.)
,接下来是 setUI()
方法,首先看到的是 @SuppressWarnings("ConstantConditions")
注解,这个注解被用于 AS 抑制 Lint 产生的 null
警告;接下来是为 PICK & CROP
按钮设置监听器,转到 pickFromGallery()
方法,如果 4.1+ 且没 READ_EXTERNAL_STORAGE
权限就申请权限(BaseActivity
中继承来的)得到权限后再调用该方法,注意这里权限组的概念,READ_EXTERNAL_STORAGE
与 WRITE_EXTERNAL_STORAGE
得到一个后另一个自动获得,有权限的话就使用官方推荐的 ACTION_GET_CONTENT
来获得图片,我个人比较偏爱打开图库的这个方法
Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
接下来就是开启另一个 Activity 了,但是他这里没有直接使用上面创建的隐式 Intent,而是又创建了一个 Intent,使用的是 Intent
的 static 方法 public static Intent createChooser (Intent target, CharSequence title)
,这个是三个参数 public static Intent createChooser (Intent target, CharSequence title, IntentSender sender)
的变异版,官方解释为
Convenience function for creating a ACTION_CHOOSER Intent. Builds a new ACTION_CHOOSER Intent that wraps the given target intent, also optionally supplying a title. If the target intent has specified FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION, then these flags will also be set in the returned chooser intent, with its ClipData set appropriately: either a direct reflection of getClipData() if that is non-null, or a new ClipData built from getData().
还算清楚,简单来说就是包装一下参数 Intent,效果是对隐式 Intent 参数不使用默认设置而是弹出一个可选择应用菜单,然而实测还是没有图库选项,谨慎使用。
既然 Intent 已经创建并传递了,那么接下来看 public void onActivityResult(int requestCode, int resultCode, Intent data)
方法。他这里并没有首先去判断参数一的 resultCode
而是先判断了 resultCode == RESULT_OK
之后进行判断 requestCode == requestMode
,如果你以为 requestMode
只是一个字段那就错了,定义处是这样的 private int requestMode = BuildConfig.RequestMode;
,而 BuildConfig
又是什么?跟踪到定义位置,其包为 com.yalantis.ucrop.sample
但是 project 视图下并找不到该文件,Google 之,这篇 文章解释的较清楚,这个文件类似与 R.java,是自动生成的,生成的依据是 app/build.gradle,可以发现这里 android 标签下定义了这样一个子标签
productFlavors {
activity {
buildConfigField("int","RequestMode", "1"
}
fragment {
buildConfigField("int","RequestMode", "2")
}
}
projectFlavors 又是什么?看 这里 ,发现这个东西是真的厉害,还可以模拟服务器接口,但是这里创建一个 activity 一个 fragment flavors 做什么?不清楚,继续向下,如果请求码为 requestMode
且返回的图片 Uri 不为 null 就开始裁剪,从 startCrop 中跳到 basisConfig 这里设置了比例相关的一些参数,这里有一个 UCrop.MIN_SIZE
用来表示图片的最小一条边的长度
接下来跳到 advancedConfig 方法,这里可以看到 UCrop 的一些高级用法,首先是 options.setCompressionFormat(Bitmap.CompressFormat.PNG);
可以转换输出格式,options.setCompressionQuality(mSeekBarQuality.getProgress());
设置保存图片的压缩比例,options.setHideBottomControls(mCheckBoxHideBottomControls.isChecked());
用来隐藏下方的用户可选择的工具栏,这样就需要程序提前设置照片裁剪的比例,在头像裁剪方面会很有用,options.setFreeStyleCropEnabled(mCheckBoxFreeStyleCrop.isChecked());
用来设置是剪切框移动还是图片移动,为 true
的话将是移动剪切框,还有一些不常用的方法可以查看文档
OK,回到 onActivityResult,下面是 requestCode == UCrop.REQUEST_CROP
就执行 handleCropResult(data)
,UCrop.REQUEST_CROP 是裁剪完成后 UCrop 会返回的请求码,转到 handleCropResult,如果 Uri 不为 null
就会调用 ResultActivity.startWithUri(SampleActivity.this, resultUri);
转到 ResultActivity
package com.yalantis.ucrop.sample;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.FileProvider;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import com.yalantis.ucrop.view.UCropView;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.util.Calendar;
import java.util.List;
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
/**
* Created by Oleksii Shliama (https://github.com/shliama).
*/
public class ResultActivity extends BaseActivity {
private static final String TAG = "ResultActivity";
private static final String CHANNEL_ID = "3000";
private static final int DOWNLOAD_NOTIFICATION_ID_DONE = 911;
public static void startWithUri(@NonNull Context context, @NonNull Uri uri) {
Intent intent = new Intent(context, ResultActivity.class);
intent.setData(uri);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_result);
Uri uri = getIntent().getData();
if (uri != null) {
try {
UCropView uCropView = findViewById(R.id.ucrop);
uCropView.getCropImageView().setImageUri(uri, null);
uCropView.getOverlayView().setShowCropFrame(false);
uCropView.getOverlayView().setShowCropGrid(false);
uCropView.getOverlayView().setDimmedColor(Color.TRANSPARENT);
} catch (Exception e) {
Log.e(TAG, "setImageUri", e);
Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
}
}
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(new File(getIntent().getData().getPath()).getAbsolutePath(), options);
setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(getString(R.string.format_crop_result_d_d, options.outWidth, options.outHeight));
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.menu_result, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_download) {
saveCroppedImage();
} else if (item.getItemId() == android.R.id.home) {
onBackPressed();
}
return super.onOptionsItemSelected(item);
}
/**
* Callback received when a permissions request has been completed.
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_STORAGE_WRITE_ACCESS_PERMISSION:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
saveCroppedImage();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
private void saveCroppedImage() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE,
getString(R.string.permission_write_storage_rationale),
REQUEST_STORAGE_WRITE_ACCESS_PERMISSION);
} else {
Uri imageUri = getIntent().getData();
if (imageUri != null && imageUri.getScheme().equals("file")) {
try {
copyFileToDownloads(getIntent().getData());
} catch (Exception e) {
Toast.makeText(ResultActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
Log.e(TAG, imageUri.toString(), e);
}
} else {
Toast.makeText(ResultActivity.this, getString(R.string.toast_unexpected_error), Toast.LENGTH_SHORT).show();
}
}
}
private void copyFileToDownloads(Uri croppedFileUri) throws Exception {
String downloadsDirectoryPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
String filename = String.format("%d_%s", Calendar.getInstance().getTimeInMillis(), croppedFileUri.getLastPathSegment());
File saveFile = new File(downloadsDirectoryPath, filename);
FileInputStream inStream = new FileInputStream(new File(croppedFileUri.getPath()));
FileOutputStream outStream = new FileOutputStream(saveFile);
FileChannel inChannel = inStream.getChannel();
FileChannel outChannel = outStream.getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
inStream.close();
outStream.close();
showNotification(saveFile);
Toast.makeText(this, R.string.notification_image_saved, Toast.LENGTH_SHORT).show();
finish();
}
private void showNotification(@NonNull File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Uri fileUri = FileProvider.getUriForFile(
this,
getString(R.string.file_provider_authorities),
file);
intent.setDataAndType(fileUri, "image/*");
List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(
intent,
PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo info : resInfoList) {
grantUriPermission(
info.activityInfo.packageName,
fileUri, FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_READ_URI_PERMISSION);
}
NotificationCompat.Builder notificationBuilder;
NotificationManager notificationManager = (NotificationManager) this
.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (notificationManager != null) {
notificationManager.createNotificationChannel(createChannel());
}
notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID);
} else {
notificationBuilder = new NotificationCompat.Builder(this);
}
notificationBuilder
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.notification_image_saved_click_to_preview))
.setTicker(getString(R.string.notification_image_saved))
.setSmallIcon(R.drawable.ic_done)
.setOngoing(false)
.setContentIntent(PendingIntent.getActivity(this, 0, intent, 0))
.setAutoCancel(true);
if (notificationManager != null) {
notificationManager.notify(DOWNLOAD_NOTIFICATION_ID_DONE, notificationBuilder.build());
}
}
@TargetApi(Build.VERSION_CODES.O)
public NotificationChannel createChannel() {
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, getString(R.string.channel_name), importance);
channel.setDescription(getString(R.string.channel_description));
channel.enableLights(true);
channel.setLightColor(Color.YELLOW);
return channel;
}
}
静态的 startWithUri 相当与一个 newInstance 方法,这里使用 startActivity 传入 Uri 启动 ResultActivity 活动,这么写比 newInstance 方法更加简洁。下来是 ResultAcitivity 的 onCreate
方法,先填充布局,layout 文件如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?colorAccent"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:title="@string/format_crop_result_d_d"
app:titleTextColor="@android:color/white"/>
<com.yalantis.ucrop.view.UCropView
android:id="@+id/ucrop"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
居然有个 UCropView,用来展示已经裁剪完的图片的,回到 ResultActivity 代码,获取到 UCropView 之后对其调用了几个操作,设置图片 URI,设置边框,设置网格,还有设置图片四周的背景色,注意这里提供颜色 int 值使用的是 android.graphics.Color.TRANSPARENT