android學習之路(七)---- 用Fan-Image-Loader實現一個炫酷的相簿功能

fandong12388發表於2015-12-06

FanGallery
一、簡介
        在上一篇博文當中,我們提出了universal-image-loader的缺點,並進行了豐富和改寫,那麼,這期我們就以上篇博文封裝的Fan-Image-Loader為基礎,實現一個相簿, 一般來說,像這種相簿功能,都有openGL來實現載入過程,以達到快速渲染的目的,但是openGL有很大的學習成本,而且擴充套件性不高,這裡我們使用Fan-Image-Loader同樣可以達到這樣的目的

這裡寫圖片描述


二、記憶體問題的解決
        對於android應用來講,記憶體始終是一個永恆的話題,但對於一個相簿app而言,圖片的載入速度和記憶體都是讓人頭疼的問題,一般的解決辦法,就是用openGL提高渲染速度,記憶體上面尚沒有更好的解決辦法,那麼在本篇例子當中,我們使用Fan-Image-Loader載入圖片,同樣可以達到openGL的渲染速度。接下來就是記憶體!記憶體!記憶體!首先我們應該明白,在android生態圈,系統對應用執行時的堆記憶體分配沒有嚴格標準,從60M~140M都有,如果app本身就消耗記憶體,那麼留給相簿和影象處理的記憶體就很少了,這時候就很容易OOM,但是另一點,android系統對應用的記憶體分配是以程式為單位的,這就給我們提供了迂迴的解決辦法:
        1.將工程分為兩個程式:app和pic,app程式就是系統預設程式,啟動時就會分配記憶體空間,pic程式的啟動由一個service啟動,相關的初始化放在service的onCreate方法當中。
        2.程式之間的通訊有兩種方式:aidl和broadcastReceiver,但是對於我們這樣一個輕量的app而言,broadcastReceiver顯然太重了,所以用aidl通訊,當我們在app程式點選某個按鈕,準備進入相簿的時候,會通過aidl提供的方法判斷相簿當中是否有圖片,如果沒有圖片,那麼就應該啟動拍照介面而不是相簿介面。
        3.由於是兩個程式,他們之間不會共用任何變數和資源,這就需要在各自的初始化入口處進行各自的初始化操作。
三、初始化
        從上面的解析當中,我們知道了用雙程式來解決記憶體問題,如何啟動一個雙程式呢?

1.自定義一個service—–FanService.java

/**
 * time: 15/12/6
 * description:pic程式的啟動入口
 *
 * @author fandong
 */
public class FanService extends Service {

    private class MixBinder extends IFanService.Stub {
        @Override
        public boolean isPhotoValidate() throws RemoteException {
            return LocalPhotoManager.getInstance().isPhotoValidate();
        }

        @Override
        public void putLocalPhoto(int code, String path) throws RemoteException {
            LocalPhotoManager.getInstance().put2Gallery(code, path);
            LocalPhotoManager.getInstance().put2Map(code, path);
        }
    }

    /*退出的時候,幹掉mix程式*/
    public static void stopFanService(Context context) {
        //1.停掉LocalPhotoManager
        LocalPhotoManager.destroy();
        //2.停掉mix程式
        Intent intent = new Intent(context, FanService.class);
        context.stopService(intent);
    }


    /*啟動mix程式*/
    public static void startFanService(Context context) {
        Intent intent = new Intent(context, FanService.class);
        context.startService(intent);
    }

    /*繫結mix程式*/
    public static void bindFanService(Context context, ServiceConnection connection) {
        Intent intent = new Intent(context, FanService.class);
        context.bindService(intent, connection, Context.BIND_AUTO_CREATE);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new MixBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //1.掃描本地圖片到記憶體當中
        LocalPhotoManager.getInstance().initialize();
        //2.初始化ImageLoader
        FanImageLoader.init(getApplicationContext(), FileUtil.getPathByType(FileUtil.DIR_TYPE_CACHE));
        //3.初始化Pinguo-image-loader當中的日誌系統
        L.writeDebugLogs(BuildConfig.DEBUG);
    }

    @Override
    public boolean onUnbind(Intent intent) {
        stopFanService(this);
        return super.onUnbind(intent);
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }
}

2.在清單檔案當中註冊,並宣告程式,核心就是android:proccess這個屬性了:

<activity
    android:name=".GalleryActivity"
    android:process=":pic"
    android:theme="@style/AppTheme.NoActionBar" />

<service
    android:name=".FanService"
    android:process=":pic" />

        可以看到,這裡我們把FanService和GalleryActivity放到了同一個程式:pic當中了,接下來就啟動:pic程式了
3.啟動pic程式,在GalleryApplication.java當中:

@Override
public void onCreate() {
    super.onCreate();
    gContext = this;
    //1.啟動pic程式
    FanService.startFanService(this);
}

        從上面的FanService.java的onCreate方法當中可以看出來,我們在此方法當中進行了本地圖片資源的初始化。

4.程式寫到這一步,完成了同一應用的雙程式執行,相當於整個app增加了一倍的堆記憶體,雖然不能根除OOM,但是也大幅降低了OOM的風險


四、實現酷炫的照片選擇效果
    1.從上面的動畫效果可以看出,當手指觸碰到裁剪區域的時候,裁剪區域會隨著相簿整體向上滑動,整個互動流程如下圖所示:
這裡寫圖片描述
    2.上面的流程當中涉及到的關鍵技術如下所示:
        2.1 裁剪介面覆蓋在RecyclerView上面,所有RecyclerView應該有一個headerView,此headerView的高度應該和裁剪區域一樣,這樣才能達到覆蓋的效果,但是recyclerView並沒有想ListView一樣新增headerView的功能,所以只能通過如下兩步操作,達到這樣的效果:

第一步、給LayoutManager設定一行的item寬度:

mLayoutManager = new GridLayoutManager(this, 4, GridLayoutManager.VERTICAL, false);
mLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int position) {
        if (position == 0) {
            return 4;//第一行寬度佔四列
        }
        return 1;
    }
});
mRecyclerView.setLayoutManager(mLayoutManager);

第二步、在adapter當中進行判斷

@Override
public GalleryViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View view;
    if (0 == viewType) {
        view = mInflater.inflate(R.layout.vw_gallery_header, parent, false);
    } else if (1 == viewType) {
        view = mInflater.inflate(R.layout.vw_gallery_camera_item, parent, false);
    } else {
        view = mInflater.inflate(R.layout.vw_gallery_item, parent, false);
    }
    view.setTag(viewType);
    return new GalleryViewHolder(view, viewType);
}
@Override
public int getItemViewType(int position) {
    return position;
}

2.2 好了,經過上面兩步,我們知道了裁剪區域是怎樣覆蓋在recyclerView上面的,那麼這一步就是體現我們滑動recyclerView時候ScrollLinearLayout如何跟隨RecyclerView進行滑動的:關鍵方法是給recyclerView設定OnScrollListener,讓我們來看看這個滑動監聽:

//2.初始化mRecyclerOnScrollListener
mRecyclerOnScrollListener = new RecyclerView.OnScrollListener() {
    float limitY = ResHelper.getDimen(R.dimen.crop_image_operation_height);

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        if (RecyclerView.SCROLL_STATE_IDLE == newState) {
            /*當recyclerview的第一個空白檢視的bottom大於 標題的bottom,那麼scrollLinearlayout應該滑動到下面*/
            boolean scrollToBottom = false;
            View targetView = mLayoutManager.getChildAt(0);
            if (0 == (int) targetView.getTag()) {
                if (limitY < targetView.getBottom()) {
                    scrollToBottom = true;
                }
            }
            mScrollLinearLayout.clipToBound(mRecyclerView::smoothScrollBy, scrollToBottom);
            System.gc();
        } else {
            if (mPopupWindow != null && mPopupWindow.isShowing()) {
                mPopupWindow.dismiss();
                mPopupWindow = null;
            }
        }
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        View view = mLayoutManager.getChildAt(0);
        if (dy > 0) {
            mScrollLinearLayout.scrollBy((float) mRecyclerView.getLastTouchY(), dx, dy);
        } else {
            if ((Integer) view.getTag() == 1) {
                if (view.getTop() >= 0) {
                    mScrollLinearLayout.scrollBy(0.f, dx, dy);
                }
            }
            if ((Integer) view.getTag() == 0) {
                if (view.getBottom() > limitY) {
                    mScrollLinearLayout.scrollBy(0.f, dx, dy);
                }
            }
        }
    }
};

    根據上面畫的流程圖再來看現在的這個滑動過程,應該不難理解,這裡就不做過多講解了

2.3 從效果圖的動態圖,我們可以看到,當選中某個圖片的時候,如果ScrollLinearLayout處於頂部懸浮狀態的時候此時ScrollLinearLayout會向上滑動,處於初始位置,而recyclerView選中的item則會滑動到ScrollLinearLayout的下面,這個功能是如何實現的呢?recyclerView的item點選事件

//3.初始化recyclerView的點選事件
mOnRecyclerItemClickListener = (position, url, clickView) -> {
    if (1 != position && position == mLastClickPosition) {
        return;
    }
    mLastClickPosition = position;
    //3.0 如果是第一個方框,則需要啟動拍照的介面
    if (1 == position && System.currentTimeMillis() - mLastClickTime > 3000) {
        mLastClickTime = System.currentTimeMillis();
        Toast.makeText(GalleryActivity.this, "點選拍照按鈕", Toast.LENGTH_SHORT).show();
        return;
    }
    int old = mGalleryAdapter.getSelectedPosition();
    mGalleryAdapter.setSelectedPosition(position);
    mGalleryAdapter.notifyItemChanged(old);
    mGalleryAdapter.notifyItemChanged(position);
    mImageCropView.setImageURI(url);
    //3.1.如果linearLayout是懸浮在上面的,就下滑至原來位置
    mScrollLinearLayout.scrollToBottom();
    //3.2.滑動item到指定位置
    mRecyclerView.smoothScrollBy(0, (int) (clickView.getTop() - mScrollLinearLayout.getFullViewHeight() + 0.5f));
};

    以上就是做到此效果的關鍵方法,詳細可以參考原始碼功能,這裡不做過多介紹
2.4 當ScrollLinearLayout處於頂部懸浮狀態的時候,此時,我想ScrollLinearLayout滑下來,只需要點選ScrollLinearLayout露出來的部分就可以了,這是如何做到的呢?就是給三個編輯按鈕新增了攔截點選事件.
第一步、GalleryActivity.java

//1.初始化mOnClickInterceptListener
mOnClickInterceptListener = () -> {
    if (mScrollLinearLayout.isTopState()) {
        mScrollLinearLayout.scrollToBottom();
        return true;
    }
    return false;
};

第二步、ImageCropView.java

@OnClick({R.id.image_crop_attach, R.id.image_crop_ratio, 
R.id.image_crop_rotate})
public void onClick(View view) {
    //1.確定是否攔截點選事件
    if (mOnClickInterceptListener != null) {
        if (mOnClickInterceptListener.clickIntercept()) {
            return;
        }
    }
……
}

相關文章