IM即時通訊專案講解(二) 自定義實現圖片選擇GalleryView

若蘭__明月發表於2018-01-03

IM即時通訊專案講解(二)--自定義實現圖片選擇GalleryView

標籤(空格分隔): 開源專案


###該系列技術課程來源慕課IM實戰

帶後臺的IM即時通訊App 全程MVP手把手打造

#####通過該課程可以學習到以下知識點

  • 1、瞭解和開發後臺專案(這個是需要長期積累的,有了這個可以說入門沒問題)
  • 2、學習到IM相關知識點,建立群、新增群、單聊、群聊
  • 3、可以學習到資料庫的相關操作(建表、表之間的關聯等知識)
  • 4、學習到MVP模式,更加深入瞭解MVP模式的架構和實現
  • 5、學習到關於IM相關的優化,比如如何快速重新整理介面
  • 6、學習到如何進行推送等相關操作(伺服器端推送,單推、群推)
  • 7。。。當然還有好多的,大家不妨去了解一下,學習到知識才是最重要的

###效果圖來一發

IM即時通訊專案講解(二)  自定義實現圖片選擇GalleryView

是不是感覺介面還是挺簡潔的呢,那下面就看下如何實現的吧,實現還真不難,反而很簡單的。

###前言

專案總結一:實現類似qq微信表情皮膚無縫切換(簡書地址) 實現類似qq微信表情皮膚無縫切換(CSDN地址)

###進入主題 #####先大致分析一下,主要有以下幾點需要我們考慮。

  • 1、如何拿到手機本地圖片
  • 2、使用什麼控制元件進行該展示
  • 3、怎麼控制照片選擇狀態
  • 4、要考慮到複用情況,畢竟手機照片可能會有好多張
  • 5、顯示出來的是方形(但是載入的圖片是形狀不規則的)

#####上述問題解決方案

  • 問題一通過LoaderManager和相關類實現
  • 問題二、四
    • 可以看到展示的是四列(可以自己定製),而且每一個圖片的展示都市相同的,那麼我們可以考慮GridView或者RecyclerView,但是從使用好感上我還是選擇了RecyclerView
    • 對於複用的問題,recyclerview也是做了很好的處理,內部強制使用Holder。(這裡就不做詳細探討)
  • 問題三
    • 其實這個我們可以放個方式來問,那就是我們的一個item裡面都有什麼佈局。
    • 首先一個ImageView,然後又CheckBox,還有就是點選的陰影效果
  • 問題五--->這個就需要自己定義一個顯示方形的控制元件了,其實就是 重新onMeasure,然後在測量的時候,傳入一個依據寬度的值(長和寬都是寬度就行了)

#####好了首先開始寫之前的問題,我們都有了相應的解決方案,對於開發中出現的問題我們在遇到的時候當場解決吧。進入實戰

###封裝實戰之前先來看下我們的item佈局並附有相關解釋 #####首先是方形控制元件SquareLayout

    //我們選擇繼承自FrameLayout  重寫onMeasure
    
     @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //高寬給父類  傳遞的測量值都是寬度  那麼就可以形成基於寬度的正方形控制元件
        if (mBaseDirection == 1) {
            super.onMeasure(widthMeasureSpec, widthMeasureSpec);
        } else if (mBaseDirection == 2) {
            super.onMeasure(heightMeasureSpec, heightMeasureSpec);
        } else {
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);

            if (heightSize == 0) {
                super.onMeasure(widthMeasureSpec, widthMeasureSpec);
                return;
            }

            if (widthSize == 0) {
                super.onMeasure(heightMeasureSpec, heightMeasureSpec);
                return;
            }
            if (widthSize > heightSize)
                super.onMeasure(heightMeasureSpec, heightMeasureSpec);
            else
                super.onMeasure(widthMeasureSpec, widthMeasureSpec);
        }
    }
    
複製程式碼

#####佈局控制元件

<?xml version="1.0" encoding="utf-8"?>
<com.mingchu.common.widget.SquareLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="1dp"
    android:background="@color/white"
    android:orientation="vertical"
    app:comAccordTo="horizontal">

    <ImageView
        android:id="@+id/im_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:contentDescription="@string/app_name" />

    <!--black_alpha_112 === #70000000-->>
    <View
        android:id="@+id/view_shade"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black_alpha_112"
        android:visibility="gone" />

    <CheckBox
        android:id="@+id/cb_select"
        android:layout_width="22dp"
        android:layout_height="22dp"
        android:layout_gravity="end"
        android:layout_margin="@dimen/len_2"
        android:button="@drawable/sel_cb_circle"
        android:clickable="false"
        android:drawablePadding="0dp"
        android:enabled="false"
        android:padding="0dp"
        app:buttonTint="@color/cb_gallery" />
</com.mingchu.common.widget.SquareLayout>
複製程式碼

cb_gallery.xml

<!--white_alpha_192 === #c0ffffff-->
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/white" android:state_checked="true" />
    <item android:color="@color/white_alpha_192" />
</selector>
複製程式碼

相比沒什麼好解釋的和之前描述的問題解答一樣,這裡就不多做解釋。直接看下面的封裝吧

###封裝RecyclerView 既然已經選擇了RecyclerView來進行我們的本地相片的列表展示,是時候來封裝一波RecyclerView,也就是在RecyclerView的基礎上自定義view了。 #####開始自定義view的第一步,繼承RecyclerView 名字就是GalleryView

 //    代表直接在java程式碼中引用如setContentView(View)
    public GalleryView(Context context) {
        super(context);
        init();
    }

//    關聯中的xml檔案中當控制元件使用
    public GalleryView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

//    在xml引用,又要自己定義一些屬性
    public GalleryView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

複製程式碼

#####第二步我們先來載入本地圖片吧 首先在載入圖片之前(為啥來個首先。。。難道還有好多麼,哈哈,不是很多,但是你要定義一個bean吧,定義我們需要取哪些資料,哪些欄位使我們需要的吧)

      /**
     * 圖片Image   jvabean
     */
    private static class Image {
        int id;  //資料的id
        String path;  //圖片的路徑
        boolean isSelect; //圖片是否選擇
        long date; //圖片建立的日期

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Image image = (Image) o;

            return path != null ? path.equals(image.path) : image.path == null;

        }

        @Override
        public int hashCode() {
            return path != null ? path.hashCode() : 0;
        }
    }

複製程式碼

載入本地並整合到集合中


    /**
     * 用於實際資料載入的Loader
     */
    private class LoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {


        //讀取圖片檔案的引數
        private final String[] IMAGE_PROJECTION = {
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.DATA,
                MediaStore.Images.Media.DATE_ADDED};

        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            if (id == LOADER_ID) {
                return new CursorLoader(getContext(),
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION,
                        null, null, IMAGE_PROJECTION[2] + " DESC");
            }
            return null;
        }

        @Override
        public void onLoadFinished(Loader<Cursor> loader, final Cursor data) {
            //當Loader載入完成的時候回撥方法
            List<Image> images = new ArrayList<>();
            if (data != null) {
                int count = data.getCount();
                if (count > 0) {
                    data.moveToFirst();
                    do {

                        //getColumnIndexOrThrow(String columnName)
                        //從零開始返回指定列名稱,如果不存在將丟擲IllegalArgumentException 異常
                        int id = data.getInt(data.getColumnIndexOrThrow(IMAGE_PROJECTION[0]));
                        //獲取到圖片本地地址
                        String path = data.getString(data.getColumnIndexOrThrow(IMAGE_PROJECTION[1]));
                        //獲取到照片的時間
                        long dateTime = data.getLong(data.getColumnIndexOrThrow(IMAGE_PROJECTION[2]));

                        File file = new File(path);
                        if (!file.exists() || file.length() < MIN_IMAGE_LEN)
                            continue;
                        //構建javabean
                        Image image = new Image();
                        image.id = id;
                        image.path = path;
                        image.date = dateTime;
                        
                        //新增到集合中
                        images.add(image);
                    } while (data.moveToNext());
                }
            }
            //載入完本地找之後進行更新資源
            updateSource(images);  
        }

        @Override
        public void onLoaderReset(Loader<Cursor> loader) {
            //當Loader銷燬或者重置
            updateSource(null);
        }
    }

複製程式碼

因為LoaderManager需要配合Activity或者Fragment,所以我們需要對外提供一個方法來傳入這兩個的例項

/**
     * 初始化方法
     *
     * @param manager  LoaderManager Loader管理器
     * @param listener 選擇改變監聽
     * @return 任何一個LOADER_ID  可以用於銷燬Loader
     */
    public int setup(LoaderManager manager, SelectedChangeListener listener) {
        mListener = listener;
        // 一個標識載入器的唯一ID    一個可選的引數以支援載入器的構建   一個LoaderManager.LoaderCallbacks的實現
        manager.initLoader(LOADER_ID, null, callback);
        return LOADER_ID;
    }
複製程式碼

相關變數

 private static final int LOADER_ID = 0x0100;
    private static final long MIN_IMAGE_LEN = 10 * 1024;  //最大的照片的大小  10MB
    private static final long MAX_IMAGE_COUNT = 9;  //最大選擇的照片的數量
複製程式碼

關於這個方法我們在後面會有介紹updateSource(images)

#####圖片已經載入到images裡面了,該是我們的展示了 無非是寫adapter和holder,然後inflater佈局,繫結控制元件,然後設定資料

 private class GalleryAdapter extends RecyclerAdapter<Image> {

        @Override
        protected ViewHolder<Image> onCreateViewHolder(View root, int viewType) {
            return new GalleryView.ViewHolder(root);
        }

        @Override
        protected int getItemViewType(int position, Image image) {
            return R.layout.cell_gallery;
        }

    }
    
    
private class ViewHolder extends RecyclerAdapter.ViewHolder<Image> {
    //圖片
    private ImageView mPic;
    //引用
    private View mShade;
    //checkbox
    private CheckBox mSelected;


    public ViewHolder(View itemView) {
        super(itemView);
        mPic = (ImageView) itemView.findViewById(R.id.im_image);
        mShade = itemView.findViewById(R.id.view_shade);
        mSelected = (CheckBox) itemView.findViewById(R.id.cb_select);
    }

    @Override
    protected void onBind(Image image) {
        //載入圖片
        Glide.with(getContext())
                .load(image.path)
                .diskCacheStrategy(DiskCacheStrategy.NONE)
                .centerCrop()
                .placeholder(R.color.grey_200)
                .into(mPic);
        //設定選擇陰影
        mShade.setVisibility(image.isSelect ? VISIBLE : INVISIBLE);
        //是否選擇
        mSelected.setChecked(image.isSelect);
        //是否顯示  未選擇的圖片checkbox不顯示
        mSelected.setVisibility(image.isSelect ? VISIBLE : INVISIBLE);
    }
}


複製程式碼

大家肯定會說繼承的RecyclerAdapterIM即時通訊專案講解(二)  自定義實現圖片選擇GalleryView(還有一個泛型Image是什麼鬼),這個不要急,是封裝的一個RecyclerView的adapter,這個在文章的結尾會給個地址的(如果篇幅過長,不要打我哈)

 //四列圖片
        setLayoutManager(new GridLayoutManager(getContext(), 4));
        setAdapter(mAdapter);  //設定adapter
複製程式碼

這個時候執行一下就是可以出現了哈,但是相關的點選邏輯我們還沒有實現哦,現在抓緊時間來實現吧。想法比較簡單,邏輯也不復雜哈。

首先是更新資料,也就是loader載入拿到的圖片集合後更新資料

  /**
     * 更新選擇的資料
     *
     * @param images 相簿中的圖片集合
     */
    private void updateSource(List<Image> images) {
        mAdapter.replace(images);
    }

複製程式碼

接下來就是item的點選實現,然後實現選擇和未選擇的邏輯

 mAdapter.setAdapterItemClickListener(new RecyclerAdapter.AdapterItemClickListener<Image>() {
            @Override
            public void onItemClick(RecyclerAdapter.ViewHolder holder, Image image) {
                if (onItemSelectClick(image)) {
                    //noinspection unchecked
                    holder.updateData(image);
                }
            }

            @Override
            public void onLongItemClick(RecyclerAdapter.ViewHolder holder, Image data) {

            }
        });
複製程式碼
 /**
     * item點選事件邏輯處理
     *
     * @param image 圖片Item
     * @return true 選擇  false 未選擇
     */
    private boolean onItemSelectClick(Image image) {
        boolean notifyRefresh;
        //判斷是否已經選擇過了
        if (mSelectedImages.contains(image)) {
            //如果選擇過了就移除這個image
            mSelectedImages.remove(image);
            //選擇標誌置為false
            image.isSelect = false;
            notifyRefresh = true; //需要重新整理的標誌置為true
        } else {
            //判斷選擇的總共大小是否超出了自定義的可選擇大小
            if (mSelectedImages.size() >= MAX_IMAGE_COUNT) {
                //Cell點選操作  如果說我們的點選是允許的  那麼更新對應的Cell狀態
                //然後去更新介面  如果不允許點選(已經達到我們最大的選擇數量)  那麼就不需要重新整理資料
                Application.showToast(String.format(
                        getResources().getText(R.string.label_gallery_select_max_size).toString(),
                        MAX_IMAGE_COUNT));
                //不需要重新整理
                notifyRefresh = false;
            } else {
                //如果不在已選擇集合中  那麼就新增到集合中
                mSelectedImages.add(image);
                image.isSelect = true; //選擇標誌置為true
                notifyRefresh = true; //需要通知重新整理
            }
        }
        //如果是需要重新整理的  新增 或者刪除都需要進行重新整理
        if (notifyRefresh)  //通知重新整理
            notifySelectChanged();
        return notifyRefresh;
    }

複製程式碼

通知重新整理一下

 /**
     * 通知選擇改變的時候重新整理
     */
    private void notifySelectChanged() {
        SelectedChangeListener listener = mListener;
        if (listener != null)
            listener.onSelectedCountChanged(mSelectedImages.size());
    }
複製程式碼

因為我們有一個最大選擇個數,這裡定義一個介面,返回我們的選擇個數

  /**
     * 圖片選擇監聽器
     */
    public interface SelectedChangeListener {
        /**
         * 選擇的個數監聽器
         *
         * @param count 圖片個數
         */
        void onSelectedCountChanged(int count);
    }
複製程式碼

因為我們最終還需要和介面進行互動,因此我們需要定義一個方法來讓外部通過這個方法獲取圖片地址(簡單的就是向外提供選擇的圖片集合的本地地址)

 /**
     * 獲取到選擇過的圖片的路徑
     *
     * @return 圖片路徑集合
     */
    public String[] getSelectedPath() {

        String[] paths = new String[mSelectedImages.size()];
        int index = 0;
        for (Image mSelectedImage : mSelectedImages) {
            paths[index++] = mSelectedImage.path;
        }
        return paths;
    }
複製程式碼

好了這裡已經實現了,基本上也就是獲取本地圖片--->封裝成我們需要的javabean--->使用recyclerview進行載入--->點選item--->改變item的狀態(是否選中,顯示checkbox)--->給外部暴露一個獲取圖片集合路徑的方法。好了思路清晰,方法明瞭。實現也是比較簡單。今天就到這了哈。關於安卓實現獲取本機的所有圖片的方法和解釋,這裡在參考閱讀中給了地址。就不詳細描述了。

###想說 鑑於本篇文章已經很長了,這裡就不貼全部的程式碼和封裝的recyclerview的程式碼了,我這裡直接提供git地址,這是從一個完整的專案中提取出來的相關總結,大家也可以下載看下,有問題可以討論。

相簿選擇自定義View--->GalleryView原始碼

封裝的RecyclerViewAdapter--->RecyclerAdapter

Adapter介面--->AdapterCallBack

###參考閱讀

相關文章