美文网首页Android
源码学习之 Matisse

源码学习之 Matisse

作者: 13kmsteady | 来源:发表于2018-07-05 22:03 被阅读163次

    前言

    作为一名一年多的 Android 开发者,面对源码阅读一直是踯躅不前。毫不夸张的说内心一直是拒绝的,担心自己没能力去读懂那该死的源码。在和朋友的交流中,我向他请教了很多关于源码阅读的技巧。提及最多的就是 搞清楚每个类的职责类与类之间的设计最终明白源码的执行过程

    重要的是通过阅读源码,针对不会的知识点进行查漏补缺,这个过程就像是打怪升级,逐渐的提高自己的技术。在朋友不断的鼓励下,终于迈出了源码阅读第一步,希望在今后的工作学习中,能把这种学习方式坚持下去。好了,现在进入今天的主题 Matisse

    简介

    Matisse 是知乎开源的一款,针对 Android 本地图像和视频的选择器。它主要有以下优点:

    • 方便在 Activity 和 Fragment 中使用

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

    • 应用不同的主题,包括内置的两种主题和自定义主题‘

    • 使用不同的图片加载器

    • 自定义文件的过滤规则

    • 方便拓展

    针对 方便拓展基佬RxImagePicker 完美的诠释了这一优点。在 RxImagePicker 中,Matisse 被抽出来放入了 RxImagePicker_Support,成为了 UI 层的基础组件。

    Matisse 的基本使用

    参考 Matisse 的 SampleActivity 中的例子,我们来一步步探索 Matisse 的实现过程。

    Matisse.from(SampleActivity.this)
                .choose(MimeType.ofImage()) 
                .theme(R.style.Matisse_Dracula)
                .countable(true)
                .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))
                .maxSelectable(9)
                .originalEnable(true)
                .maxOriginalSize(10)
                .imageEngine(new Glide4Engine())
                .forResult(REQUEST_CODE_CHOOSE);
    
    • 调用 from() 获取 Matisse 的一个实例。

    • 调用 choose() 方法获取 SelectionCreator,进行一些列的参数配置。如主题、最大选择数量。参数的保存是由全局单例 SelectionSpec 完成的。

    • 通过 forResult() 去打开 MatisseActivity

    接下来我们看下每个类的设计

    1. Matisse

      
      public final class Matisse {
      
       private final WeakReference<Activity> mContext;
          private final WeakReference<Fragment> mFragment;
      
          private Matisse(Activity activity) {
              this(activity, null);
          }
      
          private Matisse(Fragment fragment) {
              this(fragment.getActivity(), fragment);
          }
      
          private Matisse(Activity activity, Fragment fragment) {
              mContext = new WeakReference<>(activity);
              mFragment = new WeakReference<>(fragment);
          }
      
          /**
           * 获取 Matisse 对象
           */
          public static Matisse from(Activity activity) {
              return new Matisse(activity);
          }
      
      
          public static Matisse from(Fragment fragment) {
              return new Matisse(fragment);
          }
      
          /**
           * 获得选择的数据
           */
          public static List<Uri> obtainResult(Intent data) {
              return data.getParcelableArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION);
          }
      
          public static List<String> obtainPathResult(Intent data) {
              return data.getStringArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION_PATH);
          }
      
      
          public SelectionCreator choose(Set<MimeType> mimeTypes) {
              return this.choose(mimeTypes, true);
          }
      
          /**
           * 获取 SelectionCreator 对象
           */
          public SelectionCreator choose(Set<MimeType> mimeTypes, boolean mediaTypeExclusive) {
              return new SelectionCreator(this, mimeTypes, mediaTypeExclusive);
          }
      
          @Nullable
          Activity getActivity() {
              return mContext.get();
          }
      
          @Nullable
          Fragment getFragment() {
              return mFragment != null ? mFragment.get() : null;
          }
      
      }
      

      可以看到,这个类的代码还是很少的。这个类的主要职责是:

      • 获取上下文对象,以弱引用的方式进行保存。

      • 获取 SelectionCreator 对象,进行参数配置。

      • 获得用户选择的数据。

    1. SelectionCreator,这是一个配置类,所以省略大部分相同的代码:

      public final class SelectionCreator {
          private final Matisse mMatisse;
          private final SelectionSpec mSelectionSpec;
      
        
          SelectionCreator(Matisse matisse, @NonNull Set<MimeType> mimeTypes, boolean mediaTypeExclusive) {
              mMatisse = matisse;
              mSelectionSpec = SelectionSpec.getCleanInstance();
              mSelectionSpec.mimeTypeSet = mimeTypes;
              mSelectionSpec.mediaTypeExclusive = mediaTypeExclusive;
              mSelectionSpec.orientation = SCREEN_ORIENTATION_UNSPECIFIED;
          }
      
           /**
           * 是否显示单媒体类型
           */
          public SelectionCreator showSingleMediaType(boolean showSingleMediaType) {
              mSelectionSpec.showSingleMediaType = showSingleMediaType;
              return this;
          }
      
         ...... 省略部分代码 
         
          public void forResult(int requestCode) {
              Activity activity = mMatisse.getActivity();
              if (activity == null) {
                  return;
              }
      
              Intent intent = new Intent(activity, MatisseActivity.class);
      
              Fragment fragment = mMatisse.getFragment();
              if (fragment != null) {
                  fragment.startActivityForResult(intent, requestCode);
              } else {
                  activity.startActivityForResult(intent, requestCode);
              }
          }
      }
      

      这个类的主要职责是:

      • 通过链式调用,进行参数配置。

      • 开启 MatisseActivity

    2. SelectionSpec

      该类主要定义了一系列可配置的参数

      public final class SelectionSpec {
      
          public List<Item> selectItems;
          public Set<MimeType> mimeTypeSet;
          public boolean mediaTypeExclusive;
          public boolean showSingleMediaType;
          @StyleRes
          public int themeId;
          public int orientation;
          public boolean countable;
          public int maxSelectable;
          public int maxImageSelectable;
          public int maxVideoSelectable;
          public List<Filter> filters;
          public boolean capture;
          public CaptureStrategy captureStrategy;
          public int spanCount;
          public int gridExpectedSize;
          public float thumbnailScale;
          public ImageEngine imageEngine;
          public boolean hasInited;
          public OnSelectedListener onSelectedListener;
          public boolean originalable;
          public int originalMaxSize;
          public OnCheckedListener onCheckedListener;
      
          private SelectionSpec() {
          }
      
          public static SelectionSpec getInstance() {
              return InstanceHolder.INSTANCE;
          }
      
          public static SelectionSpec getCleanInstance() {
              SelectionSpec selectionSpec = getInstance();
              selectionSpec.reset();
              return selectionSpec;
          }
      
         ...... 省略部分代码
         
          private static final class InstanceHolder {
              private static final SelectionSpec INSTANCE = new SelectionSpec();
          }
      }
      

    到此,我们应进入 MatisseActivity 中一看究竟,让我们思考下展示的图片是如何获取的,带着这样的疑问向 MatisseActivity 迈进吧。

    MatisseActivity

    public class MatisseActivity extends AppCompatActivity implements
            AlbumCollection.AlbumCallbacks,
            AdapterView.OnItemSelectedListener,
            MediaSelectionFragment.SelectionProvider,
            View.OnClickListener,
            AlbumMediaAdapter.CheckStateListener,
            AlbumMediaAdapter.OnMediaClickListener,
            AlbumMediaAdapter.OnPhotoCapture {
            
            @Override
            protected void onCreate(@Nullable Bundle savedInstanceState) {
                
            ...... 省略部分代码
    
    
            // 拍照功能
            if (mSpec.capture) {
                mMediaStoreCompat = new MediaStoreCompat(this);
                if (mSpec.captureStrategy == null)
                    throw new RuntimeException("Don't forget to set CaptureStrategy.");
                mMediaStoreCompat.setCaptureStrategy(mSpec.captureStrategy);
            }
    
            
            // 资源文件夹的适配器
            mAlbumsAdapter = new AlbumsAdapter(this, null, false);
            
            // 资源文件夹的 Spinner
            mAlbumsSpinner = new AlbumsSpinner(this);
            mAlbumsSpinner.setOnItemSelectedListener(this);
            mAlbumsSpinner.setSelectedTextView((TextView) findViewById(R.id.selected_album));
            mAlbumsSpinner.setPopupAnchorView(findViewById(R.id.toolbar));
            mAlbumsSpinner.setAdapter(mAlbumsAdapter);
            
            // 资源文件夹的数据源
            mAlbumCollection.onCreate(this, this);
            mAlbumCollection.onRestoreInstanceState(savedInstanceState);
            mAlbumCollection.loadAlbums(); // 加载资源文件夹
    
            updateBottomToolbar();
        }
    }        
    

    Matisse 进行数据的获取是通过 Loader 机制完成的。Loader 是官方在 3.0 之后推荐的加载 ContentProvider 资源的最佳使用方式。方便我们在 Activity 和 Fragment 异步加载数据,在这里安利一篇关于 Loader 的文章。

    1. AlbumSpinner

      AlbumSpinner 是对资源文件夹的 ListPopubWindow 和显示文件夹名称的 TextView 进行了一层封装。最终把点击事件的处理,通过接口的方式暴露给 MatisseActivity,展示该资源文件夹下所有的图片。

      /**
       * 每次点击 ListPopuWindow 都会触发
       *
       * @param parent
       * @param view
       * @param position
       * @param id
       */
      @Override
      public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
          mAlbumCollection.setStateCurrentSelection(position);
          mAlbumsAdapter.getCursor().moveToPosition(position);
          Album album = Album.valueOf(mAlbumsAdapter.getCursor());
          if (album.isAll() && SelectionSpec.getInstance().capture) {
              album.addCaptureCount();
          }
          onAlbumSelected(album);
      }
      
    2. AlbumAdapter

      继承自 CursorAdapter ,为 ListPopubWindow 提供数据。

    3. AlbumCollection

      调用 loadAlbums(),去查询资源文件夹的数据,具体的查询过程是交给 AlbumLoader 执行。

      最后将查询的结果通过 AlbumCallbacks 接口回调给 MatisseActivity,为 AlbumAdapter 提供数据并默认显示第一个文件夹下的图片。

       /**
       * 资源文件夹,数据查询完成的回调
       *
       * @param cursor
       */
      @Override
      public void onAlbumLoad(final Cursor cursor) {
          mAlbumsAdapter.swapCursor(cursor);
          // select default album.
          Handler handler = new Handler(Looper.getMainLooper());
          handler.post(new Runnable() {
      
              @Override
              public void run() {
                  cursor.moveToPosition(mAlbumCollection.getCurrentSelection());
                  mAlbumsSpinner.setSelection(MatisseActivity.this,
                          mAlbumCollection.getCurrentSelection());
                  Album album = Album.valueOf(cursor);
                  if (album.isAll() && SelectionSpec.getInstance().capture) {
                      album.addCaptureCount();
                  }
                  onAlbumSelected(album);
              }
          });
      }
      

    现在资源文件夹的数据已经获取到了,照片墙的数据又是如何获得的哪?答案就在 onAlbumSelected() 中。

    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);
                Fragment fragment = MediaSelectionFragment.newInstance(album);
                getSupportFragmentManager()
                        .beginTransaction()
                        .replace(R.id.container, fragment, MediaSelectionFragment.class.getSimpleName())
                        .commitAllowingStateLoss();
            }
        }
    

    可以看到照片墙的实现是通过 MediaSelectionFragment 进行展示的。

    MediaSelectionFragment

    显示照片墙的 Fragment,布局中只有一个 RecyclerView。

    在初始化 MediaSelectionFragment 的时候,传入了一个 Album 对象,这个对象的作用又是做什么的呢?让我们继续追踪 MediaSelectionFragment 中的代码。

     @Override
        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
            Album album = getArguments().getParcelable(EXTRA_ALBUM);
    
            mAdapter = new AlbumMediaAdapter(getContext(),
                    mSelectionProvider.provideSelectedItemCollection(), mRecyclerView);
            mAdapter.registerCheckStateListener(this); // 注册 CheckView 是否选中的监听事件
            mAdapter.registerOnMediaClickListener(this); // 注册图片的点击事件
            mRecyclerView.setHasFixedSize(true);
    
            int spanCount;
            SelectionSpec selectionSpec = SelectionSpec.getInstance();
            if (selectionSpec.gridExpectedSize > 0) {
                spanCount = UIUtils.spanCount(getContext(), selectionSpec.gridExpectedSize);
            } else {
                spanCount = selectionSpec.spanCount;
            }
            mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), spanCount));
    
            int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing);
            mRecyclerView.addItemDecoration(new MediaGridInset(spanCount, spacing, false));
            mRecyclerView.setAdapter(mAdapter);
            mAlbumMediaCollection.onCreate(getActivity(), this);
            mAlbumMediaCollection.load(album, selectionSpec.capture); // 加载媒体数据
        }
    

    原来 Album 对象最终传递到了 AlbumMediaCollection 中。

    1. AlbumMediaCollection

      和查询资源文件夹的套路一样,都是通过 Loader 机制完成。

      根据传入的 Album 对象,获取该文件夹的 Id 作为查询参数,通过 AlbumMediaLoader 去查询数据。

      将最终查询的结果,通过 AlbumMediaCallbacks 接口暴露给 MediaSelectionFragment 。

    2. AlbumMediaAdapter

      为 RecyclerView 提供数据。以下是绑定数据的代码。

      @Override
      protected void onBindViewHolder(final RecyclerView.ViewHolder holder, Cursor cursor) {
          if (holder instanceof CaptureViewHolder) {
              CaptureViewHolder captureViewHolder = (CaptureViewHolder) holder;
              Drawable[] drawables = captureViewHolder.mHint.getCompoundDrawables();
              TypedArray ta = holder.itemView.getContext().getTheme().obtainStyledAttributes(
                      new int[]{R.attr.capture_textColor});
              int color = ta.getColor(0, 0);
              ta.recycle();
      
              for (int i = 0; i < drawables.length; i++) {
                  Drawable drawable = drawables[i];
                  if (drawable != null) {
                      final Drawable.ConstantState state = drawable.getConstantState();
                      if (state == null) {
                          continue;
                      }
      
                      Drawable newDrawable = state.newDrawable().mutate();
                      newDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
                      newDrawable.setBounds(drawable.getBounds());
                      drawables[i] = newDrawable;
                  }
              }
              captureViewHolder.mHint.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3]);
          } else if (holder instanceof MediaViewHolder) {
              MediaViewHolder mediaViewHolder = (MediaViewHolder) holder;
      
              final Item item = Item.valueOf(cursor);
              mediaViewHolder.mMediaGrid.preBindMedia(new MediaGrid.PreBindInfo(
                      getImageResize(mediaViewHolder.mMediaGrid.getContext()),
                      mPlaceholder,
                      mSelectionSpec.countable,
                      holder
              ));
              mediaViewHolder.mMediaGrid.bindMedia(item); // 绑定数据
              mediaViewHolder.mMediaGrid.setOnMediaGridClickListener(this); // 设置条目的点击事件
              setCheckStatus(item, mediaViewHolder.mMediaGrid);
          }
      }
      

      在这个类中,还声明了以下 3 个接口。具体的实现有 MatisseActivity 完成。

      
      // 修改底部工具栏的状态,更改已选择条目的集合数据
       public interface CheckStateListener {
          void onUpdate();
      }
      
      // 处理点击条目的跳转
      public interface OnMediaClickListener {
          void onMediaClick(Album album, Item item, int adapterPosition);
      }
      
      // 相机按钮的点击事件
      public interface OnPhotoCapture {
          void capture();
      }
      
    3. MediaGrid

      每个条目显示的View。自定义的 ViewGroup(包含 ImageView,CheckView,显示视频时长的 TextView,是否是 Gif 标志的 ImageView)

      进行数据具体的绑定工作。并将点击事件通过接口暴露给 AlbumMediaAdapter

      public interface OnMediaGridClickListener {
      
          // ImageView 的点击事件
          void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder);
          
          // CheckView 的点击事件
          void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder);
      }
      

    通过这么多接口的回调,减少了每个类的代码,使得每个类的逻辑更加清晰。这样的设计的确值得学习和借鉴。

    至此照片墙的部分了解的差不多了,让我们去看下预览界面的设计。

    BasePreviewActivity

    打开预览界面有两种方式:

    1. 点击图片,直接进入的预览界面 AlbumPreviewActivity

       // 点击图片
          @Override
          public void onMediaClick(Album album, Item item, int adapterPosition) {
              Intent intent = new Intent(this, AlbumPreviewActivity.class);
              intent.putExtra(AlbumPreviewActivity.EXTRA_ALBUM, album); // 该图片对应的文件夹对象
              intent.putExtra(AlbumPreviewActivity.EXTRA_ITEM, item);
              intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle()); 
              intent.putExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable);
              startActivityForResult(intent, REQUEST_CODE_PREVIEW);
          }
      
    2. 点击 CheckView,选中照片,再点击底部的预览按钮 SelectedPreviewActivity

      @Override
      public void onClick(View v) {
          // 点击预览按钮
          if (v.getId() == R.id.button_preview) {
              Intent intent = new Intent(this, SelectedPreviewActivity.class);
              intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle());
              intent.putExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable);
              startActivityForResult(intent, REQUEST_CODE_PREVIEW);
      
          }
      }    
      

    通过方式 1 进入的 AlbumPreviewActivity ,携带的参数有 该图片对应的文件夹对象。在其内部通过 Loader 机制去加载预览图片的数据:

    @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            if (!SelectionSpec.getInstance().hasInited) {
                setResult(RESULT_CANCELED);
                finish();
                return;
            }
            mCollection.onCreate(this, this);
            // 点击的图片,所属的文件夹对象
            Album album = getIntent().getParcelableExtra(EXTRA_ALBUM);
            mCollection.load(album); // 加载数据
    
            Item item = getIntent().getParcelableExtra(EXTRA_ITEM); // 点击的图片对象
            if (mSpec.countable) {
                mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item));
            } else {
                mCheckView.setChecked(mSelectedCollection.isSelected(item));
            }
            updateSize(item);
        }
    

    通过方式 2 进入SelectedPreviewActivity,直接把携带的数据,作为数据源进行展示

    @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            if (!SelectionSpec.getInstance().hasInited) {
                setResult(RESULT_CANCELED);
                finish();
                return;
            }
            
            Bundle bundle = getIntent().getBundleExtra(EXTRA_DEFAULT_BUNDLE);
            List<Item> selected = bundle.getParcelableArrayList(SelectedItemCollection.STATE_SELECTION);
            mAdapter.addAll(selected);
            mAdapter.notifyDataSetChanged();
            if (mSpec.countable) {
                mCheckView.setCheckedNum(1);
            } else {
                mCheckView.setChecked(true);
            }
            mPreviousPos = 0;
            updateSize(selected.get(0));
        }
    

    由于同样都是预览界面,代码的重复性比较多,便抽取了一个 BasePreviewActivity。该主要是由 ViewPager 和 Fragment 组成。这部分的代码就比较容易阅读了。

    总结

    在 Matisse 中,有许多关于自定义 View 的知识,如自定义的 CheckViewCheckRadioView。关于自定义 View 这方面的知识,也非常值得我们学习。

    相关文章

      网友评论

        本文标题:源码学习之 Matisse

        本文链接:https://www.haomeiwen.com/subject/oqrjuftx.html