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;
}
}
……
}