- 整个流程解析
- 自定义控件的解析,里面几乎涉及到所有的自定义控件相关的知识点,是一个非常棒的实践
- 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);
}复制代码