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 做选择框,值得注意的是除了 RadioButtonRadioGroup 还可以含有其他元素,而 RadioButtontools: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_STORAGEWRITE_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

uCrop 源码剖析的更多相关文章

  1. jQuery之Deferred源码剖析

    一.前言 大约在夏季,我们谈过ES6的Promise(详见here),其实在ES6前jQuery早就有了Promise,也就是我们所知道的Deferred对象,宗旨当然也和ES6的Promise一样, ...

  2. Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现

    声明:本文为原创博文,转载请注明出处. Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线程 ...

  3. Apache Spark源码剖析

    Apache Spark源码剖析(全面系统介绍Spark源码,提供分析源码的实用技巧和合理的阅读顺序,充分了解Spark的设计思想和运行机理) 许鹏 著   ISBN 978-7-121-25420- ...

  4. 基于mybatis-generator-core 1.3.5项目的修订版以及源码剖析

    项目简单说明 mybatis-generator,是根据数据库表.字段反向生成实体类等代码文件.我在国庆时候,没事剖析了mybatis-generator-core源码,写了相当详细的中文注释,可以去 ...

  5. STL"源码"剖析-重点知识总结

    STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点略多 :) 1.STL概述 STL提供六大组件,彼此可以组合 ...

  6. SpringMVC源码剖析(四)- DispatcherServlet请求转发的实现

    SpringMVC完成初始化流程之后,就进入Servlet标准生命周期的第二个阶段,即“service”阶段.在“service”阶段中,每一次Http请求到来,容器都会启动一个请求线程,通过serv ...

  7. 自己实现多线程的socket,socketserver源码剖析

    1,IO多路复用 三种多路复用的机制:select.poll.epoll 用的多的两个:select和epoll 简单的说就是:1,select和poll所有平台都支持,epoll只有linux支持2 ...

  8. Java多线程9:ThreadLocal源码剖析

    ThreadLocal源码剖析 ThreadLocal其实比较简单,因为类里就三个public方法:set(T value).get().remove().先剖析源码清楚地知道ThreadLocal是 ...

  9. JS魔法堂:mmDeferred源码剖析

    一.前言 avalon.js的影响力愈发强劲,而作为子模块之一的mmDeferred必然成为异步调用模式学习之旅的又一站呢!本文将记录我对mmDeferred的认识,若有纰漏请各位指正,谢谢.项目请见 ...

随机推荐

  1. .NET控件命名规范

    一.基本数据类型前缀 数据类型    数据类型简写 Array    arr Boolean    bln Byte    byt Char    chr DateTime    dtm Decima ...

  2. PTA练习题之7-2 求交错序列前N项和(15 分)

    本题要求编写程序,计算交错序列 1-2/3+3/5-4/7+5/9-6/11+... 的前N项之和. 输入格式: 输入在一行中给出一个正整数N. 输出格式: 在一行中输出部分和的值,结果保留三位小数. ...

  3. 使用Android Studio和Gradle编译NDK项目之Experimental Plugin User Guide

    转载自:http://tools.android.com/tech-docs/new-build-system/gradle-experimental Introduction The new exp ...

  4. 理解Underscore中的flatten函数

    最近是在所在实习公司的第一个sprint,有个朋友又请假了,所以任务比较重,一直这么久都没怎么更新了,这个周末赖了个床,纠结了一会儿决定还是继续写这个系列,虽然比较乏味,但是学到的东西还是很多的. 之 ...

  5. 在WAS控制台,环境下添加新的虚拟主机别名

    错误现象: 11/16/13 16:52:22:612 CST] 00000021 util          W com.ibm.ws.webcontainer.util.VirtualHostCo ...

  6. 牛客网多校训练第三场 C - Shuffle Cards(Splay / rope)

    链接: https://www.nowcoder.com/acm/contest/141/C 题意: 给出一个n个元素的序列(1,2,...,n)和m个操作(1≤n,m≤1e5),每个操作给出两个数p ...

  7. PHP数组和字符串相互转换以及判断字符串长度

    这里只介绍最常用的方法: $array=explode(separator,$string); $string=implode(glue,$array); explode() 函数用来将字符串打散成数 ...

  8. CMD命令不完全版

    cmd命令: 感谢GSC大佬提供命令 clearmgr : 清理垃圾 taskmgr : 任务管理器 eventvwr : 事件管理器 shutdown -s -t 10 : 关机 shutdown ...

  9. 【题解】洛谷P1967 [NOIP2013TG] 货车运输(LCA+kruscal重构树)

    洛谷P1967:https://www.luogu.org/problemnew/show/P1967 思路 感觉2013年D1T3并不是非常难 但是蒟蒻还是WA了一次 从题目描述中看出每个点之间有许 ...

  10. 【oracle使用笔记1】SQL报的常见错误

    项目中使用最多的就是oracle数据库了,在实际的开发中书写SQL时遇到过许多错误,趁着现在不太忙,把之前遇到的总结一下,以后遇到的会持续更新总结. 1. ORA-00001:违反唯一约束条件 [原因 ...