概述
这篇博客来重点关注Matisse里的自定义View,在MatisseActivity里,看到的逻辑十分流畅,是因为将细节部分都已经封装到了这些不同的组件中了,接下来一起看看这些组件的具体实现。
一、AlbumSpinner
AlbumSpinner用来在MatisseActivity里实现不同媒体文件夹的选取,使用ListPopupWindow作为UI显示,使用AlbumsAdapter返回的Cursor作为数据源。
1.数据源
AlbumSpinner里的setAdapter方法,传入了一个CursorAdapter
public void setAdapter(CursorAdapter adapter) {
mListPopupWindow.setAdapter(adapter);
mAdapter = adapter;
}
在MatisseActivity里,传入了这个Adapter
mAlbumsSpinner.setAdapter(mAlbumsAdapter);
mAlbumsAdapter是AlbumsAdapter的实例,AlbumsAdapter继承自CursorAdapter。
2.UI显示
mListPopupWindow = new ListPopupWindow(context, null, R.attr.listPopupWindowStyle);
mListPopupWindow.setModal(true);
float density = context.getResources().getDisplayMetrics().density;
mListPopupWindow.setContentWidth((int) (216 * density));
mListPopupWindow.setHorizontalOffset((int) (16 * density));
mListPopupWindow.setVerticalOffset((int) (-48 * density));
3.交互
交互主要集中在对于资源目录的点击事件,在AlbumSpinner里处理了一部分,然后通过onItemSelectedListener将点击事件回调给MatisseActivity,在上一篇中提到过这个接口。
mListPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
AlbumsSpinner.this.onItemSelected(parent.getContext(), position);
if (mOnItemSelectedListener != null) {
mOnItemSelectedListener.onItemSelected(parent, view, position, id);
}
}
});
private void onItemSelected(Context context, int position) {
mListPopupWindow.dismiss();
Cursor cursor = mAdapter.getCursor();
cursor.moveToPosition(position);
Album album = Album.valueOf(cursor);
String displayName = album.getDisplayName(context);
if (mSelected.getVisibility() == View.VISIBLE) {
mSelected.setText(displayName);
} else {
if (Platform.hasICS()) {
mSelected.setAlpha(0.0f);
mSelected.setVisibility(View.VISIBLE);
mSelected.setText(displayName);
mSelected.animate().alpha(1.0f).setDuration(context.getResources().getInteger(
android.R.integer.config_longAnimTime)).start();
} else {
mSelected.setVisibility(View.VISIBLE);
mSelected.setText(displayName);
}
}
}
Album是文件夹的实体类,封装了文件夹的名字、封面图片等信息。
二、CheckView
CheckView继承自View,在onMeasure里设置了宽高,使其为正方形
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// fixed size 48dp x 48dp
int sizeSpec = MeasureSpec.makeMeasureSpec((int) (SIZE * mDensity), MeasureSpec.EXACTLY);
super.onMeasure(sizeSpec, sizeSpec);
}
重点关注onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// draw outer and inner shadow
initShadowPaint();
canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
(STROKE_RADIUS + STROKE_WIDTH / 2 + SHADOW_WIDTH) * mDensity, mShadowPaint);
// draw white stroke
canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
STROKE_RADIUS * mDensity, mStrokePaint);
// draw content
if (mCountable) {
if (mCheckedNum != UNCHECKED) {
initBackgroundPaint();
canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
BG_RADIUS * mDensity, mBackgroundPaint);
initTextPaint();
String text = String.valueOf(mCheckedNum);
int baseX = (int) (canvas.getWidth() - mTextPaint.measureText(text)) / 2;
int baseY = (int) (canvas.getHeight() - mTextPaint.descent() - mTextPaint.ascent()) / 2;
canvas.drawText(text, baseX, baseY, mTextPaint);
}
} else {
if (mChecked) {
initBackgroundPaint();
canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
BG_RADIUS * mDensity, mBackgroundPaint);
mCheckDrawable.setBounds(getCheckRect());
mCheckDrawable.draw(canvas);
}
}
// enable hint
setAlpha(mEnabled ? 1.0f : 0.5f);
}
首先初始化阴影画笔
private void initShadowPaint() {
if (mShadowPaint == null) {
mShadowPaint = new Paint();
mShadowPaint.setAntiAlias(true);
// all in dp
float outerRadius = STROKE_RADIUS + STROKE_WIDTH / 2;
float innerRadius = outerRadius - STROKE_WIDTH;
float gradientRadius = outerRadius + SHADOW_WIDTH;
float stop0 = (innerRadius - SHADOW_WIDTH) / gradientRadius;
float stop1 = innerRadius / gradientRadius;
float stop2 = outerRadius / gradientRadius;
float stop3 = 1.0f;
mShadowPaint.setShader(
new RadialGradient((float) SIZE * mDensity / 2,
(float) SIZE * mDensity / 2,
gradientRadius * mDensity,
new int[]{Color.parseColor("#00000000"), Color.parseColor("#0D000000"),
Color.parseColor("#0D000000"), Color.parseColor("#00000000")},
new float[]{stop0, stop1, stop2, stop3},
Shader.TileMode.CLAMP));
}
}
接着在画上内阴影和外阴影
canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
(STROKE_RADIUS + STROKE_WIDTH / 2 + SHADOW_WIDTH) * mDensity, mShadowPaint);
接着在描边
canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
STROKE_RADIUS * mDensity, mStrokePaint);
最后在填上内容
三、SquareFrameLayout
是一个继承自FrameLayout的自定义ViewGroup,保证为正方形
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
四、MediaGridInset
继承自RecyclerView.ItemDecoration,用来自定义RecyclerView的分割线,保证各个方向的间距一样
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view); // item position
int column = position % mSpanCount; // item column
if (mIncludeEdge) {
// spacing - column * ((1f / spanCount) * spacing)
outRect.left = mSpacing - column * mSpacing / mSpanCount;
// (column + 1) * ((1f / spanCount) * spacing)
outRect.right = (column + 1) * mSpacing / mSpanCount;
if (position < mSpanCount) { // top edge
outRect.top = mSpacing;
}
outRect.bottom = mSpacing; // item bottom
} else {
// column * ((1f / spanCount) * spacing)
outRect.left = column * mSpacing / mSpanCount;
// spacing - (column + 1) * ((1f / spanCount) * spacing)
outRect.right = mSpacing - (column + 1) * mSpacing / mSpanCount;
if (position >= mSpanCount) {
outRect.top = mSpacing; // item top
}
}
}
五、PreviewViewPager
继承自ViewPager,用来处理滑动冲突问题,如果是ImageViewTouch的事件实例,就先交给ImageViewTouch处理
@Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ImageViewTouch) {
return ((ImageViewTouch) v).canScroll(dx) || super.canScroll(v, checkV, dx, x, y);
}
return super.canScroll(v, checkV, dx, x, y);
}
}
六、RoundedRectangleImageView
圆角矩形类,继承自AppCompatImageView。
在onMeasure里确定了范围
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mRectF.set(0.0f, 0.0f, getMeasuredWidth(), getMeasuredHeight());
mRoundedRectPath.addRoundRect(mRectF, mRadius, mRadius, Path.Direction.CW);
}
在onDraw里进行裁剪
@Override
protected void onDraw(Canvas canvas) {
canvas.clipPath(mRoundedRectPath);
super.onDraw(canvas);
}
七、MediaGrid
MediaGrid就是在图片选择界面看到的一个个图片的容器,它继承自上面提到的SquareFrameLayout,属性有缩略图、CheckView等一些列可见组件,同时进行了高度的封装,在Adapter里的调用更加方便。
public class MediaGrid extends SquareFrameLayout implements View.OnClickListener {
private ImageView mThumbnail;
private CheckView mCheckView;
private ImageView mGifTag;
private TextView mVideoDuration;
private Item mMedia;
private PreBindInfo mPreBindInfo;
private OnMediaGridClickListener mListener;
public MediaGrid(Context context) {
super(context);
init(context);
}
public MediaGrid(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
LayoutInflater.from(context).inflate(R.layout.media_grid_content, this, true);
mThumbnail = (ImageView) findViewById(R.id.media_thumbnail);
mCheckView = (CheckView) findViewById(R.id.check_view);
mGifTag = (ImageView) findViewById(R.id.gif);
mVideoDuration = (TextView) findViewById(R.id.video_duration);
mThumbnail.setOnClickListener(this);
mCheckView.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (mListener != null) {
if (v == mThumbnail) {
mListener.onThumbnailClicked(mThumbnail, mMedia, mPreBindInfo.mViewHolder);
} else if (v == mCheckView) {
mListener.onCheckViewClicked(mCheckView, mMedia, mPreBindInfo.mViewHolder);
}
}
}
public void preBindMedia(PreBindInfo info) {
mPreBindInfo = info;
}
public void bindMedia(Item item) {
mMedia = item;
setGifTag();
initCheckView();
setImage();
setVideoDuration();
}
public Item getMedia() {
return mMedia;
}
private void setGifTag() {
mGifTag.setVisibility(mMedia.isGif() ? View.VISIBLE : View.GONE);
}
private void initCheckView() {
mCheckView.setCountable(mPreBindInfo.mCheckViewCountable);
}
public void setCheckEnabled(boolean enabled) {
mCheckView.setEnabled(enabled);
}
public void setCheckedNum(int checkedNum) {
mCheckView.setCheckedNum(checkedNum);
}
public void setChecked(boolean checked) {
mCheckView.setChecked(checked);
}
private void setImage() {
if (mMedia.isGif()) {
SelectionSpec.getInstance().imageEngine.loadGifThumbnail(getContext(), mPreBindInfo.mResize,
mPreBindInfo.mPlaceholder, mThumbnail, mMedia.getContentUri());
} else {
SelectionSpec.getInstance().imageEngine.loadThumbnail(getContext(), mPreBindInfo.mResize,
mPreBindInfo.mPlaceholder, mThumbnail, mMedia.getContentUri());
}
}
private void setVideoDuration() {
if (mMedia.isVideo()) {
mVideoDuration.setVisibility(VISIBLE);
mVideoDuration.setText(DateUtils.formatElapsedTime(mMedia.duration / 1000));
} else {
mVideoDuration.setVisibility(GONE);
}
}
public void setOnMediaGridClickListener(OnMediaGridClickListener listener) {
mListener = listener;
}
public void removeOnMediaGridClickListener() {
mListener = null;
}
public interface OnMediaGridClickListener {
void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder);
void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder);
}
public static class PreBindInfo {
int mResize;
Drawable mPlaceholder;
boolean mCheckViewCountable;
RecyclerView.ViewHolder mViewHolder;
public PreBindInfo(int resize, Drawable placeholder, boolean checkViewCountable,
RecyclerView.ViewHolder viewHolder) {
mResize = resize;
mPlaceholder = placeholder;
mCheckViewCountable = checkViewCountable;
mViewHolder = viewHolder;
}
}
}
通过这篇博客,对于Matisse的UI控件部分就比较熟悉了,下一篇博客,一起去看数据的加载机制。
最后
以上就是眼睛大夏天为你收集整理的Android进阶 -- 知乎Matisse源码解析(三)的全部内容,希望文章能够帮你解决Android进阶 -- 知乎Matisse源码解析(三)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复