Android--選擇多張圖片,支持拖拽刪除谜叹、排序匾寝、預(yù)覽圖片

這篇博客主要寫仿微信朋友圈選擇圖片發(fā)朋友圈。整個功能包括加載圖片荷腊,顯示圖片艳悔、相冊文件夾、預(yù)覽圖片女仰,九宮格顯示已經(jīng)選擇好的圖片等等猜年。

效果圖

2019-03-28_11_45_57 [320i].gif

截圖1 截圖2 截圖 3 截圖 4
異步加載圖片
圖片文件夾
選擇圖片
已選擇的圖片
具體思路(知識點):
1. 異步加載相冊圖片;  
2. 自定義相冊文件夾疾忍;  
3. 支持單選乔外、多選(最多9張)圖片;  
4. ItemTouchHelper實現(xiàn)拖拽一罩、排序杨幼、刪除;  
5. 對Canvas畫布操作聂渊、變換差购,結(jié)合屬性動畫,實現(xiàn)圖片的放大汉嗽、縮小等欲逃;  

加載圖片肯定是異步加載,耗時任務(wù)诊胞。android系統(tǒng)默認(rèn)提供了一個Loader(Android Loader機(jī)制全面詳解及源碼淺析https://blog.csdn.net/axi295309066/article/details/52536960)請大家搓這篇文章暖夭,看了你會對Loader機(jī)制會有個全面的認(rèn)識锹杈。在結(jié)合我這篇文章,如何去加載相冊里的圖片迈着? 顯示圖片是RecyclerView竭望,第一個位置顯示的是一個拍照默認(rèn)的圖片,利用RecyclerView支持多種不同類型的布局裕菠,把第一個位置單獨提取出來咬清,去設(shè)置一個只有一張拍照圖片的布局。

  private LoaderManager.LoaderCallbacks<Cursor> mLoaderCallbacks = new LoaderManager.LoaderCallbacks<Cursor>() {
        private final String[] IMAGE_PROJECTION = {
                MediaStore.Images.Media.DATA,
                MediaStore.Images.Media.DISPLAY_NAME,
                MediaStore.Images.Media.DATE_ADDED,
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.MINI_THUMB_MAGIC,
                MediaStore.Images.Media.BUCKET_DISPLAY_NAME};
            
                
    //創(chuàng)建一個CursorLoader奴潘,去異步加載相冊的圖片
    @NonNull
    @Override
    public Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
        return new CursorLoader(SelectImageActivity.this,
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION,
                null, null, IMAGE_PROJECTION[2] + " DESC");
    }

    @Override
    public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
        if (data != null) {
            ArrayList<Image> images = new ArrayList<>();
            //是否顯示照相圖片
            if (mHasCamera) {
                //添加到第一個的位置(默認(rèn))
                images.add(new Image());
            }
            ImageFolder defaultFolder = new ImageFolder();
            defaultFolder.setName("全部照片");
            defaultFolder.setPath("");
            mImageFolders.add(defaultFolder);

            int count = data.getCount();
            if (count > 0) {
                data.moveToFirst();
                do {
                    String path = data.getString(data.getColumnIndexOrThrow(IMAGE_PROJECTION[0]));
                    String name = data.getString(data.getColumnIndexOrThrow(IMAGE_PROJECTION[1]));
                    long dateTime = data.getLong(data.getColumnIndexOrThrow(IMAGE_PROJECTION[2]));
                    int id = data.getInt(data.getColumnIndexOrThrow(IMAGE_PROJECTION[3]));
                    String thumbPath = data.getString(data.getColumnIndexOrThrow(IMAGE_PROJECTION[4]));
                    String bucket = data.getString(data.getColumnIndexOrThrow(IMAGE_PROJECTION[5]));

                    Image image = new Image();
                    image.setPath(path);
                    image.setName(name);
                    image.setDate(dateTime);
                    image.setId(id);
                    image.setThumbPath(thumbPath);
                    image.setFolderName(bucket);

                    images.add(image);

                    //如果是被選中的圖片
                    if (mSelectedImages.size() > 0) {
                        for (Image i : mSelectedImages) {
                            if (i.getPath().equals(image.getPath())) {
                                image.setSelect(true);
                            }
                        }
                    }
                    //設(shè)置圖片分類的文件夾
                    File imageFile = new File(path);
                    File folderFile = imageFile.getParentFile();
                    ImageFolder folder = new ImageFolder();
                    folder.setName(folderFile.getName());
                    folder.setPath(folderFile.getAbsolutePath());
                    if (!mImageFolders.contains(folder)) {
                        folder.getImages().add(image);
                        //默認(rèn)相冊封面
                        folder.setAlbumPath(image.getPath());
                        mImageFolders.add(folder);
                    } else {
                        // 更新
                        ImageFolder imageFolder = mImageFolders.get(mImageFolders.indexOf(folder));
                        imageFolder.getImages().add(image);
                    }
                } while (data.moveToNext());
            }
            addImagesToAdapter(images);
            //全部照片
            defaultFolder.getImages().addAll(images);
            if (mHasCamera) {
                defaultFolder.setAlbumPath(images.size() > 1 ? images.get(1).getPath() : null);
            } else {
                defaultFolder.setAlbumPath(images.size() > 0 ? images.get(0).getPath() : null);
            }
            //刪除掉不存在的旧烧,在于用戶選擇了相片,又去相冊刪除
            if (mSelectedImages.size() > 0) {
                List<Image> rs = new ArrayList<>();
                for (Image i : mSelectedImages) {
                    File f = new File(i.getPath());
                    if (!f.exists()) {
                        rs.add(i);
                    }
                }
                mSelectedImages.removeAll(rs);
            }
        }
        mImageFolderView.setImageFolders(mImageFolders);
        addImageFoldersToAdapter();
    }

    @Override
    public void onLoaderReset(@NonNull Loader<Cursor> loader) {

    }
};

以上代碼主要是如何加載圖片画髓,設(shè)置圖片的不同類型文件夾掘剪,我們打開系統(tǒng)相冊也可以看到不同類型的圖片文件夾。

image3.png

看這張圖奈虾,這個相冊文件夾是一個自定義的View夺谁,包含了兩個部分,一個是ShadowView肉微,顯示時會有個陰影部分匾鸥,二是顯示相冊文件夾(RecyclerView)。相冊文件夾的顯示和隱藏是一個平移動畫碉纳,并不是底部彈出一個Dialog或者PopupWindow勿负。我認(rèn)為微信團(tuán)隊的做法也不是。

ImageFolderView

public class ImageFolderView extends FrameLayout implements OnItemClickListener {
private View mShadowView;
private String mShadowViewColor = "#50000000";
private RecyclerView mImageFolderRv;
private List<ImageFolder> mImageFolders;
private ImageFolderViewListener mListener;
private int mImageFolderHeight;
private boolean mShow;

public ImageFolderView(Context context) {
    this(context, null);
}

public ImageFolderView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}

public ImageFolderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mShadowView = new View(context);
    mShadowView.setBackgroundColor(Color.parseColor(mShadowViewColor));
    mImageFolderRv = ( RecyclerView ) inflate(context, R.layout.image_folder_layout, null);
    //設(shè)置LayoutParams
    FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
            FrameLayout.LayoutParams.MATCH_PARENT);
    layoutParams.gravity = Gravity.BOTTOM;
    mImageFolderRv.setLayoutParams(layoutParams);
    //設(shè)置布局管理器setLayoutManager
    mImageFolderRv.setLayoutManager(new LinearLayoutManager(context));
    addView(mShadowView);
    addView(mImageFolderRv);
    //開始不顯示陰影
    mShadowView.setAlpha(0f);
    mShadowView.setVisibility(GONE);

}

public void setImageFolders(List<ImageFolder> imageFolders) {
    mImageFolders = imageFolders;
}

public void setAdapter(ImageFolderAdapter adapter) {
    if (adapter == null) {
        throw new NullPointerException("adapter not null劳曹!");
    }
    mImageFolderRv.setAdapter(adapter);
    adapter.setItemClickListener(this);
}

public void setListener(ImageFolderViewListener listener) {
    this.mListener = listener;
}

/**
 * 顯示
 */
public void show() {
    if (mShow) {
        return;
    }
    if (mListener != null) {
        mListener.onShow();
    }
    mShow = true;
    mShadowView.setVisibility(VISIBLE);
    ObjectAnimator animator = ObjectAnimator.ofFloat(mImageFolderRv,
            "translationY", mImageFolderHeight, 0);
    animator.setInterpolator(new AccelerateDecelerateInterpolator());
    animator.setDuration(388);
    animator.start();

    ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(mShadowView, "alpha", 0f, 1f);
    alphaAnimator.setDuration(388);
    alphaAnimator.start();

}

/**
 * 隱藏
 */
public void hide() {
    if (!mShow) {
        return;
    }
    if (mListener != null) {
        mListener.onDismiss();
    }
    ObjectAnimator animator = ObjectAnimator.ofFloat(mImageFolderRv,
            "translationY", 0, mImageFolderHeight);
    animator.setInterpolator(new AccelerateDecelerateInterpolator());
    animator.setDuration(388);
    animator.start();

    ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(mShadowView, "alpha", 1f, 0f);
    alphaAnimator.setDuration(388);
    alphaAnimator.start();

    alphaAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            mShow = false;
            mShadowView.setVisibility(GONE);

        }
    });
}

public boolean isShowing() {
    return mShow;
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //獲取高度
    int height = MeasureSpec.getSize(heightMeasureSpec);
    mImageFolderHeight = ( int ) (height * 0.9f);
    ViewGroup.LayoutParams params = mImageFolderRv.getLayoutParams();
    params.height = mImageFolderHeight;
    mImageFolderRv.setLayoutParams(params);
    //開始的時候奴愉,移下去
    mImageFolderRv.setTranslationY(mImageFolderHeight);
}


@Override
public void onItemClick(int position) {
    if (mListener != null) {
        mListener.onSelect(this, mImageFolders.get(position));
        hide();
    }
}

public interface ImageFolderViewListener {
    void onSelect(ImageFolderView imageFolderView, ImageFolder imageFolder);

    void onDismiss();

    void onShow();
}}

只是粘貼了部分代碼和提供了思路,實現(xiàn)了大體的功能铁孵。還有的就是選擇具體的圖片躁劣、預(yù)覽圖片,九宮格動態(tài)顯示已經(jīng)選擇好的圖片等等库菲。對RecyclerView的封裝账忘,沒有寫,我是拿來直接用的熙宇,因為對RecyclerView的封裝很早之前就寫好了鳖擒,現(xiàn)在我對RecycleView封裝和擴(kuò)展會做成一個Module,直接導(dǎo)入到項目中使用烫止。大家可以看看源碼蒋荚,看我是如何封裝RecycleView的。這個SelectImage的Demo馆蠕,對RecycleView封裝和擴(kuò)展并不是很全期升,但是大家可以先看看基礎(chǔ)的惊奇,抽取公共的ViewHolder和Adapter,怎么去添加分割線播赁。

圖片拖拽刪除颂郎、排序主要是借助了ItemTouchHelper這個類,具體看實現(xiàn)代碼容为,注釋寫很詳細(xì)

 private ItemTouchHelper mItemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
        @Override
        public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {

        // 獲取觸摸響應(yīng)的方向   包含兩個 1.拖動dragFlags 2.側(cè)滑刪除swipeFlags
        // 代表只能是向左側(cè)滑刪除乓序,當(dāng)前可以是這樣ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT
        int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
        int dragFlags;
        if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
            dragFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT | ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        } else {
            dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        }
        return makeMovementFlags(dragFlags, swipeFlags);
    }

    /**
     * 拖動的時候不斷的回調(diào)方法
     */
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        //獲取到原來的位置
        int fromPosition = viewHolder.getAdapterPosition();
        //獲取到拖到的位置
        int targetPosition = target.getAdapterPosition();
        if (fromPosition < targetPosition) {
            for (int i = fromPosition; i < targetPosition; i++) {
                Collections.swap(mSelectImages, i, i + 1);
            }
        } else {
            for (int i = fromPosition; i > targetPosition; i--) {
                Collections.swap(mSelectImages, i, i - 1);
            }
        }
        mAdapter.notifyItemMoved(fromPosition, targetPosition);
        return true;
    }

    /**
     * 側(cè)滑刪除后會回調(diào)的方法
     */
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        int position = viewHolder.getAdapterPosition();
        mSelectImages.remove(position);
        mAdapter.notifyItemRemoved(position);
    }
});

源碼地址https://github.com/StevenYan88/SelectImage

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市坎背,隨后出現(xiàn)的幾起案子替劈,更是在濱河造成了極大的恐慌,老刑警劉巖得滤,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陨献,死亡現(xiàn)場離奇詭異,居然都是意外死亡懂更,警方通過查閱死者的電腦和手機(jī)湿故,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來膜蛔,“玉大人,你說我怎么就攤上這事脖阵≡砉桑” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵命黔,是天一觀的道長呜呐。 經(jīng)常有香客問我,道長悍募,這世上最難降的妖魔是什么蘑辑? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮坠宴,結(jié)果婚禮上洋魂,老公的妹妹穿的比我還像新娘。我一直安慰自己喜鼓,他們只是感情好副砍,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著庄岖,像睡著了一般豁翎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上隅忿,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天号杏,我揣著相機(jī)與錄音,去河邊找鬼克握。 笑死却妨,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的发魄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼疑苫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起纷责,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤捍掺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后再膳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挺勿,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年喂柒,在試婚紗的時候發(fā)現(xiàn)自己被綠了不瓶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡灾杰,死狀恐怖蚊丐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情艳吠,我是刑警寧澤麦备,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站昭娩,受9級特大地震影響凛篙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜栏渺,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一呛梆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧磕诊,春花似錦填物、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至神僵,卻和暖如春雁刷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背保礼。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工沛励, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留责语,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓目派,卻偏偏與公主長得像坤候,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子企蹭,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354