ucop源码解析一

主要从以下三部分去解析ucrop的源码
  1. 整个流程解析
  2. 自定义控件的解析,里面几乎涉及到所有的自定义控件相关的知识点,是一个非常棒的实践
  3. native层的代码分析

最后会根据项目中的实际需求给出一个ucop示例。

sbs:开始第一部分分析

SampleActivity   extend BaseActivity(主要处理6.0动态权限的申请,不是本文的重点,具体知识点可以参考http://www.jianshu.com/p/d4a9855e92d3)

s1:布局文件:


其中的线条边框使用自定义shape完成:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <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>复制代码

其中使用到了radiogroup+radiobutton的组合以及seekbar。

s2:代码流程

点击pick&crop按钮,会去开启可以查看图片的应用。

Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(Intent.createChooser(intent, getString(R.string.label_select_picture)), REQUEST_SELECT_PICTURE);复制代码

之后在onactivityResult中会获取该图片uri

if (requestCode == REQUEST_SELECT_PICTURE) {
    final Uri selectedUri = data.getData();
    if (selectedUri != null) {
        startCropActivity(data.getData());
    } else {
        Toast.makeText(SampleActivity.this, R.string.toast_cannot_retrieve_selected_image, Toast.LENGTH_SHORT).show();
    }
}复制代码

之后会调用startCropActivity(params=图片的uri),在该方法中会去设置ucrop参数(实质是设置intent参数)

private void startCropActivity(@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);

    uCrop.start(SampleActivity.this);
}复制代码

之后会去调用uCrop.start(activity)方法(在uCrop.of(xxx)方法中会去初始化一个intent)

 */
public static UCrop of(@NonNull Uri source, @NonNull Uri destination) {
    return new UCrop(source, destination);
}

private UCrop(@NonNull Uri source, @NonNull Uri destination) {
    mCropIntent = new Intent();
    mCropOptionsBundle = new Bundle();
    mCropOptionsBundle.putParcelable(EXTRA_INPUT_URI, source);
    mCropOptionsBundle.putParcelable(EXTRA_OUTPUT_URI, destination);
}复制代码

ucop的start(xxx)方法中以startActivityForResult的方式开启UcopActivity(注意requestcode=REQUEST_CROP)

public void start(@NonNull Activity activity) {
    start(activity, REQUEST_CROP);
}

public void start(@NonNull Activity activity, int requestCode) {
    activity.startActivityForResult(getIntent(activity), requestCode);
}
public Intent getIntent(@NonNull Context context) {
    mCropIntent.setClass(context, UCropActivity.class);
    mCropIntent.putExtras(mCropOptionsBundle);
    return mCropIntent;
}复制代码

UcropActivity的布局文件如下:

<RelativeLayout
    android:id="@+id/ucrop_photobox"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/ucrop_color_toolbar"
        android:minHeight="?attr/actionBarSize">

        <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:text="@string/ucrop_label_edit_photo"
            android:textColor="@color/ucrop_color_toolbar_widget"/>

    </android.support.v7.widget.Toolbar>

    <FrameLayout
        android:id="@+id/ucrop_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/wrapper_controls"
        android:layout_below="@+id/toolbar"
        android:background="@color/ucrop_color_crop_background">

        <ImageView
            android:id="@+id/image_view_logo"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_gravity="center"
            app:srcCompat="@drawable/ucrop_vector_ic_crop"
            tools:background="@drawable/ucrop_vector_ic_crop"
            tools:ignore="MissingPrefix"/>

        <com.yalantis.ucrop.view.UCropView
            android:id="@+id/ucrop"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:alpha="0"/>

    </FrameLayout>

</RelativeLayout>复制代码

需要注意 android:layout_above="@+id/wrapper_controls"后面会用到。



在oncreate方法中会去调用以下4个方法

final Intent intent = getIntent();
setupViews(intent);
setImageData(intent);
setInitialState();
addBlockingView();复制代码

在setupViews中首先会把intent中的数据读出来。也就是在ucrop中设置的各项参数。

mStatusBarColor = intent.getIntExtra(UCrop.Options.EXTRA_STATUS_BAR_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_statusbar));
mToolbarColor = intent.getIntExtra(UCrop.Options.EXTRA_TOOL_BAR_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_toolbar));
mActiveWidgetColor = intent.getIntExtra(UCrop.Options.EXTRA_UCROP_COLOR_WIDGET_ACTIVE, ContextCompat.getColor(this, R.color.ucrop_color_widget_active));
mToolbarWidgetColor = intent.getIntExtra(UCrop.Options.EXTRA_UCROP_WIDGET_COLOR_TOOLBAR, ContextCompat.getColor(this, R.color.ucrop_color_toolbar_widget));
mToolbarCancelDrawable = intent.getIntExtra(UCrop.Options.EXTRA_UCROP_WIDGET_CANCEL_DRAWABLE, R.drawable.ucrop_ic_cross);
mToolbarCropDrawable = intent.getIntExtra(UCrop.Options.EXTRA_UCROP_WIDGET_CROP_DRAWABLE, R.drawable.ucrop_ic_done);
mToolbarTitle = intent.getStringExtra(UCrop.Options.EXTRA_UCROP_TITLE_TEXT_TOOLBAR);
mToolbarTitle = mToolbarTitle != null ? mToolbarTitle : getResources().getString(R.string.ucrop_label_edit_photo);
mLogoColor = intent.getIntExtra(UCrop.Options.EXTRA_UCROP_LOGO_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_default_logo));
mShowBottomControls = !intent.getBooleanExtra(UCrop.Options.EXTRA_HIDE_BOTTOM_CONTROLS, false);
mRootViewBackgroundColor = intent.getIntExtra(UCrop.Options.EXTRA_UCROP_ROOT_VIEW_BACKGROUND_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_crop_background));复制代码

其中包括:

状态栏颜色

toolbar的颜色

当前选中的图标(标识控制图片旋转放缩以及控制选择框的比例)的颜色

toolbar中的图标的颜色(通过drawable的setColorFilter设置)

toolbar中取消的图标

toolbar中确定的图标

toolbar中的title文案(默认是裁剪)

logo的颜色

是否显示下方控制布局

toolbar以下部分的背景色

之后会去调用setupAppBar设置toolbar部分:

private void setupAppBar() {
    setStatusBarColor(mStatusBarColor);

    final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);

    // Set all of the Toolbar coloring
    toolbar.setBackgroundColor(mToolbarColor);
    toolbar.setTitleTextColor(mToolbarWidgetColor);

    final TextView toolbarTitle = (TextView) toolbar.findViewById(R.id.toolbar_title);
    toolbarTitle.setTextColor(mToolbarWidgetColor);
    toolbarTitle.setText(mToolbarTitle);

    // Color buttons inside the Toolbar
    Drawable stateButtonDrawable = ContextCompat.getDrawable(this, mToolbarCancelDrawable).mutate();
    stateButtonDrawable.setColorFilter(mToolbarWidgetColor, PorterDuff.Mode.SRC_ATOP);
    toolbar.setNavigationIcon(stateButtonDrawable);

    setSupportActionBar(toolbar);
    final ActionBar actionBar = getSupportActionBar();
    if (actionBar != null) {
        actionBar.setDisplayShowTitleEnabled(false);
    }
}复制代码

其中

Drawable stateButtonDrawable = ContextCompat.getDrawable(this, mToolbarCancelDrawable).mutate();
stateButtonDrawable.setColorFilter(mToolbarWidgetColor, PorterDuff.Mode.SRC_ATOP);
toolbar.setNavigationIcon(stateButtonDrawable);复制代码

用于控制取消按钮的颜色

final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
    actionBar.setDisplayShowTitleEnabled(false);
}复制代码

用于去掉toolbar中的"Ucrop"字样,只显示默认的"裁剪"。

设置完toolbar部分之后会去设置toolbar下方的rootView,调用initiateRootView方法

private void initiateRootViews() {
    mUCropView = (UCropView) findViewById(R.id.ucrop);
    mGestureCropImageView = mUCropView.getCropImageView();
    mOverlayView = mUCropView.getOverlayView();
    mGestureCropImageView.setTransformImageListener(mImageListener);

    ((ImageView) findViewById(R.id.image_view_logo)).setColorFilter(mLogoColor, PorterDuff.Mode.SRC_ATOP);
    findViewById(R.id.ucrop_frame).setBackgroundColor(mRootViewBackgroundColor);
}复制代码

其中

   mGestureCropImageView = mUCropView.getCropImageView();
    mOverlayView = mUCropView.getOverlayView();
    mGestureCropImageView.setTransformImageListener(mImageListener);复制代码

用于初始化自定控件UcopView(后续会有该自定义控件的源码分析)

之后的代码是用于设置logo颜色以及rootView的背景色。

根据ucop中设置的是否显示底部控制栏会去设置底部状态栏,代码如下:

if (mShowBottomControls) {
    ViewGroup photoBox = (ViewGroup) findViewById(R.id.ucrop_photobox);
    View.inflate(this, R.layout.ucrop_controls, photoBox);

    mWrapperStateAspectRatio = (ViewGroup) findViewById(R.id.state_aspect_ratio);
    mWrapperStateAspectRatio.setOnClickListener(mStateClickListener);
    mWrapperStateRotate = (ViewGroup) findViewById(R.id.state_rotate);
    mWrapperStateRotate.setOnClickListener(mStateClickListener);
    mWrapperStateScale = (ViewGroup) findViewById(R.id.state_scale);
    mWrapperStateScale.setOnClickListener(mStateClickListener);

    mLayoutAspectRatio = (ViewGroup) findViewById(R.id.layout_aspect_ratio);
    mLayoutRotate = (ViewGroup) findViewById(R.id.layout_rotate_wheel);
    mLayoutScale = (ViewGroup) findViewById(R.id.layout_scale_wheel);

    setupAspectRatioWidget(intent);
    setupRotateWidget();
    setupScaleWidget();
    setupStatesWrapper();
}复制代码

在ucop的布局文件中是没有底部控制栏这一部分的

ViewGroup photoBox = (ViewGroup) findViewById(R.id.ucrop_photobox);
    View.inflate(this, R.layout.ucrop_controls, photoBox);复制代码

以上代码会将控制栏添加到ucropAcitivity的布局中,读到这一部分时会疑惑,为啥inflate完了之后,刚好就在底部呢,其实是通过ucropActivity布局中的android:layout_above="@+id/wrapper_controls" 来实现的。

之后的代码用于初始化底部控制栏:其中setupAspectratioWidget用于设置控制裁剪框的长宽比的部分。

private void setupAspectRatioWidget(@NonNull Intent intent) {
    //所有可用的裁剪框的长宽比在aspectRatioList中,aspectRationSelectedByDefault用于记录选中的item的index,默认是0;
    int aspectRationSelectedByDefault = intent.getIntExtra(UCrop.Options.EXTRA_ASPECT_RATIO_SELECTED_BY_DEFAULT, 0);
    ArrayList<AspectRatio> aspectRatioList = intent.getParcelableArrayListExtra(UCrop.Options.EXTRA_ASPECT_RATIO_OPTIONS);

    //如果用户没有设置,那么默认有1:1,3:4,原始比例,3:2,16:9几个可以设置的长宽比。默认选中的是原始比例。
    if (aspectRatioList == null || aspectRatioList.isEmpty()) {
        aspectRationSelectedByDefault = 2;

        aspectRatioList = new ArrayList<>();
        aspectRatioList.add(new AspectRatio(null, 1, 1));
        aspectRatioList.add(new AspectRatio(null, 3, 4));
        aspectRatioList.add(new AspectRatio(getString(R.string.ucrop_label_original).toUpperCase(),
                CropImageView.SOURCE_IMAGE_ASPECT_RATIO, CropImageView.SOURCE_IMAGE_ASPECT_RATIO));
        aspectRatioList.add(new AspectRatio(null, 3, 2));
        aspectRatioList.add(new AspectRatio(null, 16, 9));
    }

    //放长宽比条目的布局
    LinearLayout wrapperAspectRatioList = (LinearLayout) findViewById(R.id.layout_aspect_ratio);

    FrameLayout wrapperAspectRatio;
    AspectRatioTextView aspectRatioTextView;
    //设置每一个长宽比TextView的lp(主要用于设置weight)
    LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT);
    lp.weight = 1;
    for (AspectRatio aspectRatio : aspectRatioList) {
        wrapperAspectRatio = (FrameLayout) getLayoutInflater().inflate(R.layout.ucrop_aspect_ratio, null);
        wrapperAspectRatio.setLayoutParams(lp);
        aspectRatioTextView = ((AspectRatioTextView) wrapperAspectRatio.getChildAt(0));
        aspectRatioTextView.setActiveColor(mActiveWidgetColor);
        aspectRatioTextView.setAspectRatio(aspectRatio);
        //将每一个长宽比的textview的父布局添加到布局中
        wrapperAspectRatioList.addView(wrapperAspectRatio);
        mCropAspectRatioViews.add(wrapperAspectRatio);
    }
    //设置当前选中状态
    mCropAspectRatioViews.get(aspectRationSelectedByDefault).setSelected(true);

    //设置每个item的点击事件
    for (ViewGroup cropAspectRatioView : mCropAspectRatioViews) {
        cropAspectRatioView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //修改裁剪的长宽比
                mGestureCropImageView.setTargetAspectRatio(
                        ((AspectRatioTextView) ((ViewGroup) v).getChildAt(0)).getAspectRatio(v.isSelected()));
                mGestureCropImageView.setImageToWrapCropBounds();
                //如果没有选中,修改选中状态
                if (!v.isSelected()) {
                    for (ViewGroup cropAspectRatioView : mCropAspectRatioViews) {
                        cropAspectRatioView.setSelected(cropAspectRatioView == v);
                    }
                }
            }
        });
    }
}复制代码

其中表示每一个长宽比的itemAspectRatioTextView是一个自定义textView

后面会有该view的源码解析

设置点击的波纹效果

<item name="android:background">?attr/selectableItemBackground</item>复制代码

setupRotateWidget(xxx)用于设置旋转图片时的滚轮

(自定义viewHorizontalProgressWheelView)后续会有该view的源码解析

private void setupRotateWidget() {
    mTextViewRotateAngle = ((TextView) findViewById(R.id.text_view_rotate));
    ((HorizontalProgressWheelView) findViewById(R.id.rotate_scroll_wheel))
            .setScrollingListener(new HorizontalProgressWheelView.ScrollingListener() {
                @Override
                public void onScroll(float delta, float totalDistance) {
                    mGestureCropImageView.postRotate(delta / ROTATE_WIDGET_SENSITIVITY_COEFFICIENT);
                }

                @Override
                public void onScrollEnd() {
                    mGestureCropImageView.setImageToWrapCropBounds();
                }

                @Override
                public void onScrollStart() {
                    mGestureCropImageView.cancelAllAnimations();
                }
            });

    ((HorizontalProgressWheelView) findViewById(R.id.rotate_scroll_wheel)).setMiddleLineColor(mActiveWidgetColor);


    findViewById(R.id.wrapper_reset_rotate).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            resetRotation();
        }
    });
    findViewById(R.id.wrapper_rotate_by_angle).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            rotateByAngle(90);
        }
    });
}复制代码

在这里主要做的事:

1:给滚轮设置监听(滚动时滚动图片,滚动结束时调整图片适应裁剪框,滚动开始时结束所有动画)

2:设置滚轮中间的竖杠的颜色为 mActiveWidgetColor,更新滚轮中间的tv的数字

3:设置了左侧复原按钮以及右侧旋转90度的监听事件


setupScaleWidget(xxx)用于设置放缩时的滚轮(跟setupRotateWidget基本一致,略过)

setupStatesWrapper()用于设置选中当前的控制按钮(是控制放缩图片,旋转图片还是控制裁剪框)其中使用到了自定义StateListDrawable

 private void setupStatesWrapper() {

    ImageView stateScaleImageView = (ImageView) findViewById(R.id.image_view_state_scale);
    ImageView stateRotateImageView = (ImageView) findViewById(R.id.image_view_state_rotate);
    ImageView stateAspectRatioImageView = (ImageView) findViewById(R.id.image_view_state_aspect_ratio);

    stateScaleImageView.setImageDrawable(new SelectedStateListDrawable(stateScaleImageView.getDrawable(), mActiveWidgetColor));
    stateRotateImageView.setImageDrawable(new SelectedStateListDrawable(stateRotateImageView.getDrawable(), mActiveWidgetColor));
    stateAspectRatioImageView.setImageDrawable(new SelectedStateListDrawable(stateAspectRatioImageView.getDrawable(), mActiveWidgetColor));
}复制代码

那么如何自定义一个StateListDrawable呢?

public class SelectedStateListDrawable extends StateListDrawable {

    private int mSelectionColor;

    public SelectedStateListDrawable(Drawable drawable, int selectionColor) {
        super();
        this.mSelectionColor = selectionColor;
        addState(new int[]{android.R.attr.state_selected}, drawable);
        addState(new int[]{}, drawable);
    }

    @Override
    protected boolean onStateChange(int[] states) {
        boolean isStatePressedInArray = false;
        for (int state : states) {
            if (state == android.R.attr.state_selected) {
                isStatePressedInArray = true;
            }
        }
        if (isStatePressedInArray) {
            super.setColorFilter(mSelectionColor, PorterDuff.Mode.SRC_ATOP);
        } else {
            super.clearColorFilter();
        }
        return super.onStateChange(states);
    }

    @Override
    public boolean isStateful() {
        return true;
    }

}
复制代码

如果我们需要给view设置选中背景,第一时间想到的应该是在shape中写一个selector,根据不同的view状态设置不同的drawable,其实这个xml文件解析完成之后就是statelistdrawable。它里面有几个比较重要的方法:

1:addState(状态数组,drawable)表示当view处于状态数组中的状态时,显示drawable。

比如ucrop中的addState(new int[]{android.R.attr.state_selected}, drawable);

实质是说selected=true,其他状态=false时,设置drawable

addState(new int[]{}, drawable);复制代码

没有任何状态时,设置也是drawable;

2:onStateChange(int[] states):参数表示当前的状态

当状态变化时会回调该方法,在上面的示例中,如果存在seleted=true,会给图片设置蒙层,通过setColorfilter的方式,否则清理掉该蒙层。

3:isStateful() :表示状态改变时时候替换drawable,需要return true。


setupImageData(xxx)用于解读ucrop中的参数,并根据给的参数去初始化裁剪图片需要的参数。比如:图片的源uri,图片裁剪完成之后的存放uri,图片的格式,裁剪框的样式,图片接受的touch事件类型等等。


setInitialState()用于设置初始状态,(其中的setWidgeState(xxx)主要是实现控制条跟控制按钮的联动,控制可见不可见状态)

private void setInitialState() {
    if (mShowBottomControls) {
        if (mWrapperStateAspectRatio.getVisibility() == View.VISIBLE) {
            setWidgetState(R.id.state_aspect_ratio);
        } else {
            setWidgetState(R.id.state_scale);
        }
    } else {
        setAllowedGestures(0);
    }
}复制代码


addBlockingView()用于给rootview添加一个点击事件隔绝板,当正在裁剪时需要隔绝用户的touch事件(通过设置隔绝板的clickable事件为true即可隔绝)

private void addBlockingView() {
    if (mBlockingView == null) {
        mBlockingView = new View(this);
        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        lp.addRule(RelativeLayout.BELOW, R.id.toolbar);
        mBlockingView.setLayoutParams(lp);
        mBlockingView.setClickable(true);
    }

    ((RelativeLayout) findViewById(R.id.ucrop_photobox)).addView(mBlockingView);
}复制代码


转载于:https://juejin.im/post/59e77625518825469c746659

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值