这篇博客来重点关注Matisse里的自定义View,在MatisseActivity里,看到的逻辑十分流畅,是因为将细节部分都已经封装到了这些不同的组件中了,接下来一起看看这些组件的具体实现。
一、AlbumSpinner
AlbumSpinner用来在MatisseActivity里实现不同媒体文件夹的选取,使用ListPopupWindow作为UI显示,使用AlbumsAdapter返回的Cursor作为数据源。
1.数据源
AlbumSpinner里的setAdapter方法,传入了一个CursorAdapter
1
2
3
4public void setAdapter(CursorAdapter adapter) { mListPopupWindow.setAdapter(adapter); mAdapter = adapter; }
在MatisseActivity里,传入了这个Adapter
1mAlbumsSpinner.setAdapter(mAlbumsAdapter);
mAlbumsAdapter是AlbumsAdapter的实例,AlbumsAdapter继承自CursorAdapter。
2.UI显示
1
2
3
4
5
6mListPopupWindow = 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,在上一篇中提到过这个接口。
1
2
3
4
5
6
7
8
9mListPopupWindow.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); } } });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21private 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里设置了宽高,使其为正方形
1
2
3
4
5
6@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方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34@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); }
首先初始化阴影画笔
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22private 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)); } }
接着在画上内阴影和外阴影
1
2canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2, (STROKE_RADIUS + STROKE_WIDTH / 2 + SHADOW_WIDTH) * mDensity, mShadowPaint);
接着在描边
1
2canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2, STROKE_RADIUS * mDensity, mStrokePaint);
最后在填上内容
三、SquareFrameLayout
是一个继承自FrameLayout的自定义ViewGroup,保证为正方形
1
2
3
4@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, widthMeasureSpec); }
四、MediaGridInset
继承自RecyclerView.ItemDecoration,用来自定义RecyclerView的分割线,保证各个方向的间距一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24@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处理
1
2
3
4
5
6
7
8@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里确定了范围
1
2
3
4
5
6@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里进行裁剪
1
2
3
4
5@Override protected void onDraw(Canvas canvas) { canvas.clipPath(mRoundedRectPath); super.onDraw(canvas); }
七、MediaGrid
MediaGrid就是在图片选择界面看到的一个个图片的容器,它继承自上面提到的SquareFrameLayout,属性有缩略图、CheckView等一些列可见组件,同时进行了高度的封装,在Adapter里的调用更加方便。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104public 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进阶内容请搜索靠谱客的其他文章。
发表评论 取消回复