uCrop 源碼剖析

GitHub: uCrop, 版本爲 2.2.2java

主要是探究一下內部對於圖片按比例的裁剪以及壓縮, 應該會更很長一段時間android

疑惑點

這裏記下一些源碼分析過程當中遇到的疑惑點git

  • sample/src/main/res/layout/activity_sample.xml 內 Toolbar 是 GONE 的,FrameLayout 寬度高度都是 0,既然都不用,留着作什麼?github

  • sample/src/main/res/layout/activity_sample.xml 內 include 引入的佈局定義了 id 屬性有什麼用?安全

  • sample app/build.gradle 中定義兩個 flavors(activity, fragment) 用來作什麼?服務器

整個項目結構

clone 下來的源代碼包含兩個 module, sample 和 ucrop, simple 是一個可運行的 module, ucrop 是其依賴的庫app

sample module

這個 module 能夠看到 ucrop 的一些用法less

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 官方解釋爲dom

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 trueide

該屬性指示該軟件是否能夠備份,爲 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

相關文章
相關標籤/搜索