知乎 Matisse 源码解析,带你探究高效图片选择库的秘密

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

目录

  • 基本介绍
  • 整体的设计和实现流程
  • 资源文件夹的加载和展示
  • 主页图片墙的实现
  • 预览界面的实现
  • 总结

一、基本介绍


Matisse 是「知乎」开源的一款十分精美的本地图像和视频选择库。

Matisse

Matisse 的代码写的相当的简洁、规范,很有学习的价值。

讲一下 Matisse 的一些优点:

  • 在 Activity 或 Fragment 都可以轻松的调用

  • 支持各种格式的图片和视频加载

  • 支持不同的样式,包括两种内置主题和自定义主题

  • 可以自定义文件的过滤规则

可以看到 Matisse 的可拓展性是非常强的,不仅可以自定义我们需要的主题,而且还可以按照需求来过滤出我们想要的文件,除此之外,Matisse 采用了建造者模式,使得我们可以通过链式调用的方式,配置各种各样的属性,使我们的图片选择更加灵活。

二、整体的设计和实现流程


在介绍 Matisse 的工作流程之前,我们先来看看几个比较重要的类,有助于我们后面的理解

类名 功能
Matisse 通过外部传入的 Activity 或 Fragment,以弱引用的形式进行保存,同时通过 from() 方法返回 SelectionCreator 进行各个参数的配置
SelectionCreator 通过建造者模式,链式配置我们需要的各种属性
MatisseActivity Matisse 首页的 Activity,将图片和视频进行展示

我们先从 Matisse 的使用入手,看看 Matisse 的工作流程。

Matisse.from(MainActivity.this)
        .choose(MimeType.allOf()) // 1、获取 SelectionCreator
        .countable(true)
        .maxSelectable(9)
        .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))
        .gridExpectedSize(getResources().getDimensionPixelSize(R.dimen.grid_expected_size))
        .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
        .thumbnailScale(0.85f)
        .imageEngine(new GlideEngine()) // 2、配置各种各样的参数
        .forResult(REQUEST_CODE_CHOOSE); // 3、打开 MatisseActivity

上面的使用代码,我们以 Activity 为例,可以分成三部分来看

  • 将外部传入的 Activity 以弱引用的形式进行保存,然后调用 choose() 获取 SelectionCreator

  • 通过链式调用的方式,配置 SelectionCreator 的各种属性,如可选择的数量、缩略图的大小、加载图片的引擎等

  • 使用从第一步中传入的 Activity 调用 startActivityForResult(),并从外部传入请求码,以便到时候返回所选择图片的 List<Uri>

具体的流程图如下:


Matisse 流程图

以上便是 Matisse 的工作流程,接下来详细的分析下相关的类。有一点要先说明一下,我下面贴出的所有类中的源码并不是完整的代码,而是将源码中与性能、兼容性、扩展性有关的代码剔除后的「核心代码」。

Matisse

public final class Matisse {

    private final WeakReference<Activity> mContext;
    private final WeakReference<Fragment> mFragment;

    private Matisse(Activity activity, Fragment fragment) {
        mContext = new WeakReference<>(activity);
        mFragment = new WeakReference<>(fragment);
    }

    public static Matisse from(Activity activity) {
        return new Matisse(activity);
    }

    public static Matisse from(Fragment fragment) {
        return new Matisse(fragment);
    }

    /**
     *  在打开 MatisseActivity 的 Activity 或 Fragment 中获取用户选择的媒体 Uri 列表
     */
    public static List<Uri> obtainResult(Intent data) {
        return data.getParcelableArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION);
    }

    public SelectionCreator choose(Set<MimeType> mimeTypes, boolean mediaTypeExclusive) {
        return new SelectionCreator(this, mimeTypes, mediaTypeExclusive);
    }

}

这个类的代码还是很简单的,将外部传入的 Activity 或 Fragment,用弱引用的形式保存,防止内存泄露。然后通过 choose() 方法返回 SelectionCreator 用于之后参数的配置。等到图片选择完成后,我们可以在 Fragment 或 Activity 中的 onActivityResult() 中通过 obtainResult() 获取我们所选择媒体的 Uri 列表。

SelectionCreator

public final class SelectionCreator {
    private final Matisse mMatisse;
    private final SelectionSpec mSelectionSpec;

    SelectionCreator(Matisse matisse, @NonNull Set<MimeType> mimeTypes) {
        mMatisse = matisse;
        mSelectionSpec = SelectionSpec.getCleanInstance();
        mSelectionSpec.mimeTypeSet = mimeTypes;
    }

    public SelectionCreator theme(@StyleRes int themeId) {
        mSelectionSpec.themeId = themeId;
        return this;
    }

    public SelectionCreator maxSelectable(int maxSelectable) {
        mSelectionSpec.maxSelectable = maxSelectable;
        return this;
    }
    // 其余方法都类似上面这两个,这里面就不贴出来了

    public void forResult(int requestCode) {
        Activity activity = mMatisse.getActivity();
        Intent intent = new Intent(activity, MatisseActivity.class);
        Fragment fragment = mMatisse.getFragment();
        if (fragment != null) {
            fragment.startActivityForResult(intent, requestCode);
        } else {
            activity.startActivityForResult(intent, requestCode);
        }
    }

}

可以看到 SelectionCreator 内部保存了 Matisse 的实例,用于获取外部调用的 Activity 或 Fragment,以及一个 SelectionSpec 类的实例,这个类封装了图片加载类中常见的参数,使得 SelectionCreator 的代码更加简洁。SelectionCreator 内部使用了建造者模式,让我们能够进行链式调用,配置各种各样的属性。最后 forResult() 里面其实就是跳转到 MatisseActivity,然后通过外部传入的 requestCode 将用户选择的媒体 Uri 列表返回给相应的 Activity 或 Fragment.

三、资源文件夹的加载和展示


Matisse 中所展示的资源都是用 Loader 机制进行加载的,Loader 机制是 Android 3.0 之后官方推荐的加载 ContentProvider 中资源的最佳方式,不仅能极大地提高我们资源加载的速度,而且还能让我们的代码变得更加的简洁。对于 Loader 机制不熟悉的同学,可以先看下这篇文章 Android Loader 机制,让你的数据加载更加高效

先附上此项操作的流程图:


继承了 Cursor 的 AlbumLoader,作为资源的加载器,通过配置与资源相关的一些参数,从而加载资源。AlbumCollection 实现了 LoaderManager.LoaderCallbacks 接口,将 AlbumLoader 作为加载器,其内部定义了 AlbumCallbacks 接口,在加载资源完成后,将包含数据的 Cursor 回调给外部调用的 MatisseActivity,然后在 MatisseActivity 中进行资源文件夹的展示。

AlbumsLoader

public class AlbumLoader extends CursorLoader {

    // content://media/external/file
    private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external");

    private static final String[] COLUMNS = {
            MediaStore.Files.FileColumns._ID,
            "bucket_id",
            "bucket_display_name",
            MediaStore.MediaColumns.DATA,
            COLUMN_COUNT};

    private static final String[] PROJECTION = {
            MediaStore.Files.FileColumns._ID,
            "bucket_id",
            "bucket_display_name",
            MediaStore.MediaColumns.DATA,
            "COUNT(*) AS " + COLUMN_COUNT};

    private static final String SELECTION =
            "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
                    + " OR "
                    + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)"
                    + " AND " + MediaStore.MediaColumns.SIZE + ">0"
                    + ") GROUP BY (bucket_id";

    private static final String[] SELECTION_ARGS = {
            String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
            String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO),
    };

    private static final String BUCKET_ORDER_BY = "datetaken DESC";

    private AlbumLoader(Context context, String selection, String[] selectionArgs) {
        super(context, QUERY_URI, PROJECTION, selection, selectionArgs, BUCKET_ORDER_BY);
    }

    public static CursorLoader newInstance(Context context) {
        return new AlbumLoader(context, SELECTION, SELECTION_ARGS);
    }

    @Override
    public Cursor loadInBackground() {
       return super.loadInBackground();
    }
}

因为在 Matisse 只需要获取到手机中的图片和视频资源,所以直接将必要的参数配置在 AlbumLoader 中,然后提供 newInstance() 方法给外部调用,获取 AlbumLoader 的实例。

AlbumCollection

public class AlbumCollection implements LoaderManager.LoaderCallbacks<Cursor> {
    private static final int LOADER_ID = 1;
    private static final String STATE_CURRENT_SELECTION = "state_current_selection";
    private WeakReference<Context> mContext;
    private LoaderManager mLoaderManager;
    private AlbumCallbacks mCallbacks;
    private int mCurrentSelection;

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        Context context = mContext.get();
        return AlbumLoader.newInstance(context);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        Context context = mContext.get();
        mCallbacks.onAlbumLoad(data);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        Context context = mContext.get();
        mCallbacks.onAlbumReset();
    }

    public void onCreate(FragmentActivity activity, AlbumCallbacks callbacks) {
        mContext = new WeakReference<Context>(activity);
        mLoaderManager = activity.getSupportLoaderManager();
        mCallbacks = callbacks;
    }

    public void loadAlbums() {
        mLoaderManager.initLoader(LOADER_ID, null, this);
    }

    public interface AlbumCallbacks {
        void onAlbumLoad(Cursor cursor);

        void onAlbumReset();
    }
}

Matisse 为了降低代码的耦合度,将一些客户端与 LoaderManager 交互的一些操作封装在 AlbumCollection 中。在 onCreate() 中,传入 Activity 用于获取 LoaderManager,加载资源完成后,在 onLoadFinished() 方法中,通过 AlbumCallbacks 的 onAlbumLoad(Cursor cursor) 方法将「包含数据的 Cursor」返回给外部调用的 MatisseActivity.

AlbumsSpinner

AlbumsSpinner 将 MatisseActivity 左上角的一组控件进行了封装,主要包括显示文件夹名称的 TextView 以及显示文件夹列表的 ListPopupWindow,相当于把一个相对完整的功能抽取出来,把逻辑操作写在里面,在 Activity 中当做一种控件来用,有点类似自定义 View.

public class AlbumsSpinner {

    private static final int MAX_SHOWN_COUNT = 6;
    private CursorAdapter mAdapter;
    private TextView mSelected;
    private ListPopupWindow mListPopupWindow;
    private AdapterView.OnItemSelectedListener mOnItemSelectedListener;

在 AlbumCollection 中返回的 Cursor,作为 AlbumsSpinner 的数据源,然后通过 AlbumsAdapter 将资源文件夹显示出来。当选中文件夹的时候,将所点击的文件夹的 position 回调给 MatisseActivity 中的 onItemSelected() 方法。

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        mAlbumCollection.setStateCurrentSelection(position);
        mAlbumsAdapter.getCursor().moveToPosition(position);
        // Album 是文件夹的实体类,封装了文件夹的名字、封面图片等信息
        Album album = Album.valueOf(mAlbumsAdapter.getCursor());
        onAlbumSelected(album);
    }

通过 AlbumsSpinner 回调出来的 position 拿到对应的文件夹的信息,然后将当前的界面进行刷新,使当前界面显示所选择的文件夹的图片。

    private void onAlbumSelected(Album album) {
        if (album.isAll() && album.isEmpty()) {
            mContainer.setVisibility(View.GONE);
            mEmptyView.setVisibility(View.VISIBLE);
        } else {
            mContainer.setVisibility(View.VISIBLE);
            mEmptyView.setVisibility(View.GONE);
            // MediaSelectionFragment 中包含一个 RecyclerView,用于显示文件夹中所有的图片
            Fragment fragment = MediaSelectionFragment.newInstance(album);
            getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.container, fragment, MediaSelectionFragment.class.getSimpleName())
                    .commitAllowingStateLoss();
        }
    }

四、主页图片墙的实现


主页的照片墙可以说是 Matisse 中最有意思的模块了,而且学习价值也是最高的。图片墙的数据源同样是通过 Loader 机制来进行加载的,实现思路也跟上一节讲的「资源文件夹的加载和展示」差不多,这里简单讲一下就好。

主页的照片墙会通过我们选择不同的资源文件夹而展示不同的图片,所以我们在选择资源文件夹的时候,便将资源文件夹的 id,传给对应的 Loader,让它对相应的资源文件进行加载。

Matisse 把图片和音频的信息封装成了实体类,并实现了 Parcelable 接口,让其序列化,通过外部传入的 Cursor,拿到对应的 Uri、媒体类型、文件大小,如果是视频的话,就获取视频播放的时长。

/**
 * 图片或音频的实体类
 */
public class Item implements Parcelable {

    public final long id;
    public final String mimeType;
    public final Uri uri;
    public final long size;
    public final long duration; // only for video, in ms

    private Item(long id, String mimeType, long size, long duration) {
        this.id = id;
        this.mimeType = mimeType;
        Uri contentUri;
        if (isImage()) {
            contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        } else if (isVideo()) {
            contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
        } else {
            // 如果不是图片也不是音频就直接当文件存储
            contentUri = MediaStore.Files.getContentUri("external");
        }
        this.uri = ContentUris.withAppendedId(contentUri, id);
        this.size = size;
        this.duration = duration;
    }

    public static Item valueOf(Cursor cursor) {
        return new Item(cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)),
                cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE)),
                cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns.SIZE)),
                cursor.getLong(cursor.getColumnIndex("duration")));
    }

}

图片墙是直接用一个 RecyclerView 进行展示的,Item 是一个继承了 SquareFrameLayout(正方形的 FrameLayout) 的自定义控件,主要包含三个部分

  • 右上角的 CheckView

  • 显示图片的 ImageView

  • 显示视频时长的 TextView

CheckView 就是右上角那个白色的小圆圈,可以理解为是一个自定义的 CheckBox,或者说是一个比较好看的复选框。我在前文中说 Matisse 的学习价值比较高,一个很重要的原因就是 Matisse 中有很多的自定义 View,能够让我们学习图片选择库的同时,学习自定义 View 的一些好的思路和做法。

那我们就来看看 CheckView 究竟是怎样实现的。

首先,CheckView 重写了 onMeasure() 方法,将宽和高都定为 48,而且为了屏幕适配性,将 48dp 乘以 density,将 dp 单位转换为像素单位。

    private static final int SIZE = 48; // dp

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int sizeSpec = MeasureSpec.makeMeasureSpec((int) (SIZE * mDensity), MeasureSpec.EXACTLY);
        super.onMeasure(sizeSpec, sizeSpec);
    }

接下来就看重头戏的 onDraw() 方法了

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 1、画出外在和内在的阴影
        initShadowPaint();
        canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
                (STROKE_RADIUS + STROKE_WIDTH / 2 + SHADOW_WIDTH) * mDensity, mShadowPaint);

        // 2、画出白色的空心圆
        canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
                STROKE_RADIUS * mDensity, mStrokePaint);

        // 3、画出圆里面的内容
        if (mCountable) {
                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);
            }
        }
    }

onDraw() 方法主要分为三个部分

  • 画出空心圆内外的阴影
    不得不说,Matisse 的细节处理真的做得特别好,为了图片选择库看起来更加美观,在空心圆的内外增加了一层辐射渐变的阴影

  • 画出白色的空心圆
    这个真没什么好讲的

  • 描绘出里面的内容
    通过我们外部配置的 mCountable 参数,来决定 CheckView 的显示方式,如果 mCountable 的值为 true 的话,便在内部描绘一层主题颜色的背景,以及代表所选择图片数量的数字,如果 mCount 的值为 false 的话,那么便描绘背景以及填入一个白色的 ✓

这部分主要是有关 Paint 的知识,以及数学方面的计算,如果对于 Paint 不是很熟悉的读者,可以看看这篇文章 HenCoder Android 开发进阶: 自定义 View 1-2 Paint 详解,顺便安利一波,凯哥的 HenCoder 教程,写得是真的好,强烈建议去好好看看。

看完了 CheckView 的实现逻辑,我们接着来看看图片墙的 Item 布局「MediaGrid」的实现逻辑,MediaGrid 是一个继承了 SquareFrameLayout(正方形的 FrameLayout)的自定义控件,可以理解为是一个拓展了复选功能(CheckView)和显示视频时长(TextView)功能的 ImageView.

我们从 MediaGrid 在 Adapter 中的使用入手,进一步看看 MediaGrid 的代码实现

mediaViewHolder.mMediaGrid.preBindMedia(new MediaGrid.PreBindInfo(
        getImageResize(mediaViewHolder.mMediaGrid.getContext()),
        mPlaceholder,
        mSelectionSpec.countable,
        holder
        ));
       mediaViewHolder.mMediaGrid.bindMedia(item);

可以看到 MediaGrid 的使用主要分两步

  • 初始化图片的公有属性(MediaGrid.preBindMedia(new MediaGrid.PreBindInfo()))

  • 将图片对应的信息进行绑定(MediaGrid.bindMedia(Item) )

PreBindInfo 是 MediaGrid 的一个静态内部类,封装了一些图片的一些公用的属性

    public static class PreBindInfo {
        int mResize; // 图片的大小
        Drawable mPlaceholder; // ImageView 的占位符
        boolean mCheckViewCountable; // √ 的图标
        RecyclerView.ViewHolder mViewHolder; // 对应的 ViewHolder

        public PreBindInfo(int resize, Drawable placeholder, boolean checkViewCountable,
                           RecyclerView.ViewHolder viewHolder) {
            mResize = resize;
            mPlaceholder = placeholder;
            mCheckViewCountable = checkViewCountable;
            mViewHolder = viewHolder;
        }
    }

Item 在上文已经介绍了,是图片或音频的实体类。第二步便是将一个包含图片信息的 Item 传给 MediaGrid,然后进行相应信息的设置。

MediaGrid 中自定义了回调的接口

    public interface OnMediaGridClickListener {

        void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder);

        void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder);
    }

当用户点击图片的时候,将点击事件回调到 Adapter,再回调到 MediaSelectionFragment,再回调到 MatisseActivity,然后打开图片的大图预览界面,你没看错,真的回调了三层,我也是一脸蒙蔽。一遇到这种情况,我就觉得 EventBus 还是挺好用的。

当点击右上角的 CheckView 的时候,便将点击事件回调到 Adapter 中,然后根据 countable 的值,来进行相应的设置(显示数字或者显示 √),然后再将对应的 Item 信息保存在 SelectedItemCollection(Item 的容器) 中。

五、预览界面的实现


打开预览界面有两种方法

  • 点击首页的某个图片

  • 选择图片之后,点击首页左下角的预览(Preview)按钮

这两种方法打开的界面看起来似乎是一样的,但实际上他们两个的实现逻辑很不一样,因此用了两个不同的 Activity.

点击首页的某张图片之后,会跳转到一个包含 ViewPager 的界面,因为对应资源文件夹中可能会有很多的图片,这时候如果将包含该文件夹中所有的图片直接传给预览界面的 Activity,这是非常不实际的。比较好的实现方式便是将「包含对应文件夹的信息的 Album」传给界面,然后再用 Loader 机制进行加载。

选择首页图片后,点击左下角的预览按钮,跳转到预览界面,因为我们选择的图片一般都比较少,所以这时候直接将「包含所有选择图片信息的 List<Item>」传给预览界面就行了。

虽然,两个 Activity 的实现逻辑不太一样,但由于都是预览界面,所以有很多相同的地方。因此,Matisse 便实现了一个 BasePreviewActivity,减少代码的冗余程度。

BasePreviewActivity 的布局主要由三部分组成

  • 右上角的 CheckView

  • 自定义的 ViewPager

  • 底部栏(包括预览(Preview)和使用按钮(Apply))

主要的代码逻辑也基本上是围绕这三个部分进行展开的。

当点击 CheckView 的时候,根据该图片是否已经被选择以及图片的类型,对 CheckView 进行相应的设置以及更新底部栏。

        mCheckView.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                Item item = mAdapter.getMediaItem(mPager.getCurrentItem());
                // 如果当前的图片已经被选择
                if (mSelectedCollection.isSelected(item)) {
                    mSelectedCollection.remove(item);
                    if (mSpec.countable) {
                        mCheckView.setCheckedNum(CheckView.UNCHECKED);
                    } else {
                        mCheckView.setChecked(false);
                    }
                } else {
                    // 判断能否添加该图片
                    if (assertAddSelection(item)) {
                        mSelectedCollection.add(item);
                        if (mSpec.countable) {
                            mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item));
                        } else {
                            mCheckView.setChecked(true);
                        }
                    }
                }
                // 更新底部栏
                updateApplyButton();
            }
        });

当用户对 ViewPager 进行左右滑动的时候,根据当前的 position 拿到对应的 Item 信息,然后对 CheckView 进行相应的设置以及切换图片。

    @Override
    public void onPageSelected(int position) {
        PreviewPagerAdapter adapter = (PreviewPagerAdapter) mPager.getAdapter();
        if (mPreviousPos != -1 && mPreviousPos != position) {
            ((PreviewItemFragment) adapter.instantiateItem(mPager, mPreviousPos)).resetView();
            // 获取对应的 Item 
            Item item = adapter.getMediaItem(position);
            if (mSpec.countable) {
                int checkedNum = mSelectedCollection.checkedNumOf(item);
                mCheckView.setCheckedNum(checkedNum);
                if (checkedNum > 0) {
                    mCheckView.setEnabled(true);
                } else {
                    mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached());
                }
            } else {
                boolean checked = mSelectedCollection.isSelected(item);
                mCheckView.setChecked(checked);
                if (checked) {
                    mCheckView.setEnabled(true);
                } else {
                    mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached());
                }
            }
            updateSize(item);
        }
        mPreviousPos = position;
    }

以上便是 BasePreviewActivity 的实现逻辑,至于它的子类 AlbumPreviewActivity(包含所有图片的预览界面)和 SelectedPreviewActivity(所选择图片的预览界面)就很简单了,大家自己看下源码就能明白了。

总结


Matisse 应该是我第一个完整啃下来的开源项目了,从一开始被 MatisseActivity 实现的一堆接口吓蒙。到后来的一步一步抽丝剥茧,从各个功能点入手,慢慢的理解了其中的代码设计以及实现思路,看完整个项目之后,对于 Matisse 的架构设计和代码质量深感佩服。

在阅读比较大型的开源项目的时候,由于这个项目你是完全陌生的,而且代码量通常都比较大,这时如果在阅读源码的时候,深陷代码细节的话,很容易让我们陷入到思维黑洞里面。如果我们从功能点入手,一步一步分析功能点是如何实现的,分析主体的逻辑,这样阅读起来就会更加轻松,也更加有成效。


猜你喜欢

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容