Picture Flash (图片放映)
考点: 数据库操作, 图片内存管理, 列表类视图, 动画, [异步线程]
功能描述:
1.创建放映专辑(专辑创建界面)
用户从手机中选定多张图片, 设置图片专辑名称后, 点击确认创建一个图片flash专辑.
1)图片列表从系统媒体库中查询获取, 按图片媒体库中的添加时间排列
2)采用两列多行的方式显示图片列表
3)用户可以输入Flash专辑的名称
4)选定多张图片并且输入专辑名称后, 点击OK按钮后创建一个图片Flash专辑存到数据库.
其他说明:
A.用户点击图片即可选定图片, 如已选中则转为未选定
B.若用户没有输入专辑名, 点击OK按钮时则Toast提示”请输入专辑名”
C.若用户未选择任何图片, 点击OK按钮时则Toast提示”请选择图片”
D.图片的加载会比较耗时, 考虑使作异步线程进行加载
图2.1
2.专辑列表浏览(主界面)
1)以列表的方式显示用户之前创建的所有专辑
2)列表每项显示专辑名, 及其中的第一张图片作为封面
3)点击列表中的专辑即跳转到图片Flash播放界面(见功能点3)
4)点击标题栏右边的”+”按钮跳转到专辑创建界面
其他说明:
A.如初始进入程序专辑列表为空, 可在列表空白处给出提示信息
B.如列表较多时加载数据需要一些时间, 考虑使用异步线程加载并显示一个loading
图 2.2
3.播放图片Flash专辑(图片Flash播放界面)
1)进入界面后即开始播放当前专辑的所有图片
2)以图片由大到小的镜头拉近动画效果(参看天猫主页的顶部Banner效果)顺序播放图片, 循环播放
3)在页面底部以 “ 当前页 / 总页数”的格式显示播放进度
4)切换到下一张图片的时机为当图片缩放至布满屏幕时(高度或宽度布满即可, 保持图片原比例), 图片起始的大小为布满大小的1.2倍
在播放其间点击屏幕任意位置即退出播放界面
图2.3
这是来到公司的第二个小项目,做一个图片专辑放映。最终实现的效果如下所示:
然后,附上程序的流程图
接着,附上项目的MUL图。
分析一下需求,要实现上述效果,需要做的有以下几点:
1. 自定义imagerloader类的实现
2. 图片路径的获取
3. 数据库的操作
4. 图片的放映
5. recylerview的使用以及图片大小的适配
要完成上述功能,首先我们必须写好一个imageloader类,一是不能动不动就OOM,二是使用起来要方便简洁。刚好想起鸿洋有一篇讲解仿微信相册的博客,于是就跑去看了。
主要是http://blog.csdn.net/lmj623565791/article/details/49300989和http://blog.csdn.net/lmj623565791/article/details/39943731。
首先是imageloader类,获取一个单例,使用一个线程进行加载,队列的调度方式为先进后出
public static ImageLoader getInstance()
{
if (mInstance == null)
{
synchronized (ImageLoader.class)
{
if (mInstance == null)
{
mInstance = new ImageLoader(1, Type.LIFO);
}
}
}
return mInstance;
}
接着是初始化工作,首先创建一个线程,不断轮训,让线程池不断查找是否有任务。
private void init(int threadCount, Type type)
{
// loop thread
mPoolThread = new Thread()
{
@Override
public void run()
{
Looper.prepare();
mPoolThreadHander = new Handler()
{
@Override
public void handleMessage(Message msg)
{
mThreadPool.execute(getTask());
try
{
mPoolSemaphore.acquire();
} catch (InterruptedException e)
{
}
}
};
// 释放一个信号量
mSemaphore.release();
Looper.loop();
}
};
mPoolThread.start();
// 获取应用程序最大可用内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 8;
mLruCache = new LruCache<String, Bitmap>(cacheSize)
{
@Override
protected int sizeOf(String key, Bitmap value)
{
return value.getRowBytes() * value.getHeight();
};
};
mThreadPool = Executors.newFixedThreadPool(threadCount);
mPoolSemaphore = new Semaphore(threadCount);
mTasks = new LinkedList<Runnable>();
mType = type == null ? Type.LIFO : type;
}
接着,便是加载图片。首先通过LruCache算法,根据图片路径从内存取出bitmap,如果为空,创建线程并将线程作为一个Task添加至线程池中。
public void loadImage(final String path, final ImageView imageView)
{
// set tag
imageView.setTag(path);
// UI线程
if (mHandler == null)
{
mHandler = new Handler()
{
@Override
public void handleMessage(Message msg)
{
ImgBeanHolder holder = (ImgBeanHolder) msg.obj;
ImageView imageView = holder.imageView;
Bitmap bm = holder.bitmap;
String path = holder.path;
if (imageView.getTag().toString().equals(path))
{
imageView.setImageBitmap(bm);
}
}
};
}
Bitmap bm = getBitmapFromLruCache(path);
if (bm != null)
{
ImgBeanHolder holder = new ImgBeanHolder();
holder.bitmap = bm;
holder.imageView = imageView;
holder.path = path;
Message message = Message.obtain();
message.obj = holder;
mHandler.sendMessage(message);
} else
{
addTask(new Runnable()
{
@Override
public void run()
{
ImageSize imageSize = getImageViewWidth(imageView);
int reqWidth = imageSize.width;
int reqHeight = imageSize.height;
Bitmap bm = decodeSampledBitmapFromResource(path, reqWidth,
reqHeight);
addBitmapToLruCache(path, bm);
ImgBeanHolder holder = new ImgBeanHolder();
holder.bitmap = getBitmapFromLruCache(path);
holder.imageView = imageView;
holder.path = path;
Message message = Message.obtain();
message.obj = holder;
// Log.e("TAG", "mHandler.sendMessage(message);");
mHandler.sendMessage(message);
mPoolSemaphore.release();
}
});
}
}
接下来,根据图片的原有宽高,缩放成指定大小的图片。故须计算一下inSampleSize。
private int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight)
{
// 源图片的宽度
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
if (width > reqWidth && height > reqHeight)
{
// 计算出实际宽度和目标宽度的比率
int widthRatio = Math.round((float) width / (float) reqWidth);
int heightRatio = Math.round((float) width / (float) reqWidth);
inSampleSize = Math.max(widthRatio, heightRatio);
}
return inSampleSize;
}
然后根据上述的inSampleSize,对图片进行缩放处理
private Bitmap decodeSampledBitmapFromResource(String pathName,int reqWidth, int reqHeight)
{
// 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(pathName, options);
// 调用上面定义的方法计算inSampleSize值
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// 使用获取到的inSampleSize值再次解析图片
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(pathName, options);
return bitmap;
}
到这里,一个基本的ImageLoader已经基本完成了。
然后,我们来到图片页面。通过Content Provider对数据进行查找,获取所有图片的路径、张数和popwindow的路片路径和首张图片的路径。
private class ScanAsyncTask extends AsyncTask<Void,Void,Void>{
@Override
protected void onPreExecute() {
if (!Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)){
view.showToast("暂无外部存储");
return;
}
view.showLoding();
}
@Override
protected Void doInBackground(Void... voids) {
String firstImage = null;
Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver mContentResolver = mContext.getContentResolver();
// 只查询jpeg和png的图片
Cursor mCursor = mContentResolver.query(mImageUri, null,
MediaStore.Images.Media.MIME_TYPE + "=? or "
+ MediaStore.Images.Media.MIME_TYPE + "=?",
new String[] { "image/jpeg", "image/png" },
MediaStore.Images.Media.DATE_ADDED+" DESC");
while (mCursor.moveToNext()){
//获取图片路径
String path = mCursor.getString(mCursor.getColumnIndex(
MediaStore.Images.Media.DATA
));
if (firstImage == null){
firstImage = path;
}
File parentFile = new File(path).getParentFile();
if (parentFile == null)
continue;
String dirPath = parentFile.getAbsolutePath();
ImageFloder imageFloder = null;
// 利用一个HashSet防止多次扫描同一个文件夹(不加这个判断,图片多起来还是相当恐怖的~~)
if (mDirPaths.contains(dirPath))
{
continue;
}else {
mDirPaths.add(dirPath);
imageFloder = new ImageFloder();
imageFloder.setDir(dirPath);
imageFloder.setFirstImagePath(path);
}
int picSize = parentFile.list(new FilenameFilter() {
@Override
public boolean accept(File file, String filename) {
return filename.endsWith(".jpg")
|| filename.endsWith(".png")
|| filename.endsWith(".jpeg");
}
}).length;
totalCount += picSize;
imageFloder.setCount(picSize);
mImageFloders.add(imageFloder);
if (picSize > mPicsSize){
mPicsSize = picSize;
mImgDir = parentFile;
}
}
mCursor.close();
mDirPaths = null;
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
view.dismissLoading();
if (mImgDir == null){
view.showToast("一张图片都没扫描到。。");
return;
}
mImgs = Arrays.asList(mImgDir.list());
view.setPictureData(mImgs,mImgDir.getAbsolutePath());
view.setPicTotalCount(totalCount+"张");
view.setImageFloders(mImageFloders);
}
}
接着,在PictureActivity通过recyclerview对数据进行展示,点击popwindow的时候,根据所选的文件路径,查找该文件夹下的图片,并对数据进行刷新。
@Override
public void setImageFloders(List<ImageFloder> list) {
mImageFloders = list;
mListImageDirPopupWindow = new ListImageDirPopupWindow(
ViewGroup.LayoutParams.MATCH_PARENT, (int) (App.sScreenHeight * 0.7),
mImageFloders, LayoutInflater.from(getApplicationContext())
.inflate(R.layout.list_dir, null));
mListImageDirPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
// 设置背景颜色变暗
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = 1.0f;
getWindow().setAttributes(lp);
}
});
// 设置选择文件夹的回调
mListImageDirPopupWindow.setOnImageDirSelected(this);
}
@Override
public void selected(ImageFloder floder) {
mImgDir = new File(floder.getDir());
mImgs = Arrays.asList(mImgDir.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
if (filename.endsWith(".jpg") || filename.endsWith(".png")
|| filename.endsWith(".jpeg"))
return true;
return false;
}
}));
mAdapter.setData(mImgs, mImgDir);
idTotalCount.setText(mImgs.size() + "张");
idChooseDir.setText(floder.getName());
mListImageDirPopupWindow.dismiss();
}
当点击右上角的时候,创建一个弹窗,用于添加所选图片的专辑名字。创建成功,则把选过的图片取消,把edittext清空,并将所选图片的路径和专辑名称存入数据库。
mBuilder = new AlertDialog.Builder(this);
mBuilder.setTitle("创建专辑")
.setView(dialog)
.setPositiveButton("确定",new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
if (!editText.getText().toString().equals("")){
mDialog.cancel();
mPresenter.createAlumb(editText.getText().toString(),mAdapter.getmSelectedImage());
editText.setText("");
mAdapter.clearSelected();
Toast.makeText(mContext,"创建成功",Toast.LENGTH_SHORT).show();
}else {
mDialog.show();
Toast.makeText(mContext,"专辑名不能为空",Toast.LENGTH_SHORT).show();
}
}
}).setNeutralButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
editText.setText("");
}
});
mDialog = mBuilder.create();
}
那么,创建成功的时候,要将数据存入数据库,那么,我们得写一个SqlHelper类,用于管理数据的增删改查。
public class DBOpenHelper extends SQLiteOpenHelper {
//创建一个picture表,有id(自增长)、图片路径、专辑名称三个属性
private static final String CREATE_TABLE_SQL = "create table picture( id " + "integer primary key autoincrement,path varchar not null, " +"alumb varchar not null)";
public DBOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL(CREATE_TABLE_SQL);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
}
}
接着就是一个service类,用于实现数据库的增删改查
public class PictureService {
DBOpenHelper mDbHelper;
SQLiteDatabase mDb;
public PictureService(Context context, int version) {
mDbHelper = new DBOpenHelper(context, "picture.db", null, version);
}
public void insert(String path,String alumb) {
mDb = mDbHelper.getWritableDatabase();
mDb.execSQL("insert into picture(path,alumb) values(?,?)",
new Object[]{path, alumb});
}
public void delete(String alumb) {
mDb = mDbHelper.getWritableDatabase();
mDb.execSQL("delete from picture where alumb=?", new String[]{alumb});
}
public List<AlumbBean> getAlumbInfo() {
mDb = mDbHelper.getWritableDatabase();
List<AlumbBean> alumbList = new ArrayList<>();
Cursor cursor = mDb.rawQuery("select * from picture",null);
boolean first = true;
String myPath = "";
String myAlumb = "";
if (cursor.moveToFirst()){
do {
String path = cursor.getString(cursor.getColumnIndex("path"));
String alumb = cursor.getString(cursor.getColumnIndex("alumb"));
ImageBean bean = new ImageBean();
bean.setPath(path)
.setAlumb(alumb);
AlumbBean alumbBean = new AlumbBean(myPath,myAlumb);
if (alumbBean.getAlumb().equals(alumb)){
myPath = myPath + "," +path;
}else {
if (first){
first = false;
}else {
alumbList.add(alumbBean);
}
myPath = path;
myAlumb = alumb;
}
}while (cursor.moveToNext());
alumbList.add(new AlumbBean(myPath,myAlumb));
}
cursor.close();
return alumbList;
}
}
到这里,我们已经可以实现专辑的创建以及图片的选择了。那么剩下的,就是主页面的展示和专辑详情页面的展示。在主页面,我们通过PictureService里的getAlumbInfo(),我们可以获取到专辑列表,我们只需配合recyclerview将数据展示出来即可。
点击主页面专辑的时候,回跳转至ImagePagerActivity
这里是一个viewpager嵌套着fragment,循环滑动的页面。
fragment通过获取到到url,对图片进行加载,配合scale动画。
由于viewpager会自动缓存左右两个页面,即使设置了setOffscreenPageLimit(0);也无效,查看源码,发现limit小于1的时候,会自动设置为1,故设置不缓存是不可行的。所以使用懒加载的方式,当页面可见的时候再加载动画,就不会出现页面跳转之后没有缩放动画了
public class ImageDetailFragment extends Fragment {
private String ImageUrl;
private ImageView img;
private Animation animation;
public static ImageDetailFragment newInstance(String imageUrl) {
final ImageDetailFragment f = new ImageDetailFragment();
final Bundle args = new Bundle();
args.putString("url", imageUrl);
f.setArguments(args);
return f;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ImageUrl = getArguments()!=null?getArguments().getString("url"):null;
getArguments().remove("url");
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.image_detail_fragment,container,false);
img = (ImageView) v.findViewById(R.id.image);
ImageLoader.getInstance(3, ImageLoader.Type.LIFO)
.loadImage(ImageUrl,img);
animation = AnimationUtils.loadAnimation(getContext(),R.anim.scale_in);
img.setClickable(true);
img.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
getActivity().finish();
}
});
return v;
}
@Override
public void onResume() {
super.onResume();
animation = AnimationUtils.loadAnimation(getContext(),R.anim.scale_in);
img.startAnimation(animation);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
if (isVisibleToUser&&animation!=null){
img.startAnimation(animation);
}
super.setUserVisibleHint(isVisibleToUser);
}
}
到这里,项目的分析基本就结束了。最后附上代码链接:
https://github.com/RuijiePan/PictureFlash.git