一、开发内容简介
最近课程要求仿照华为系统相册做一个android相册客户端,我称之为智能相册(智能是指其使用了机器学习的人脸识别、人脸检测和分类算法)。本着反正实验报告写了也是写了的心态,还有自己在写的过程中搜索资料的时候发现其实好像有关于相册列表获取这部分内容网上讲的其实挺少的,虽然我的办法并不是多么高明,在这里还是给大家分享一下,希望能对一些人起到帮助。
二、开发内容要求
智能相册APP参照华为系统相册的样式和功能,主要分为三部分内容:
照片:显示手机存储的所有图片
相册:将手机的所有图片按不同的相册分类,点击各个相册查看其包含图片
分类:根据智能算法对手机存储中的所有图片进行整理与分类,例如“人像”、“地点”、“事物”等。
三、智能相册效果展示
四、开发过程详述
(1)顶部导航栏的实现
顶部导航栏借用了github上的一个开源第三方库wasabeef/awesome-android-ui实现,选取其中的SmartTabLayout控件,注意这里有andoridx和legacy android support library版本,我选取的是legacy android support library版本,具体实现如下:
build.gradle文件添加依赖
dependencies { compile 'com.ogaclejapan.smarttablayout:library:1.7.0@aar' //Optional: see how to use the utility. compile 'com.ogaclejapan.smarttablayout:utils-v4:1.7.0@aar' //Deprecated since 1.7.0 compile 'com.ogaclejapan.smarttablayout:utils-v13:1.7.0@aar' }
activity_main.xml文件添加代码如下:
MainActivity.java文件设置Adapter和ViewPager,代码如下:
FragmentPagerItemAdapter adapter = new FragmentPagerItemAdapter( getSupportFragmentManager(), FragmentPagerItems.with(this) .add("照片", PageFragment1.class) .add("相册", PageFragment2.class) .add("发现", PageFragment3.class) .create()); ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager); viewPager.setAdapter(adapter); SmartTabLayout viewPagerTab = (SmartTabLayout) findViewById(R.id.viewpagertab); viewPagerTab.setViewPager(viewPager);
到这里就顺利完成了顶部导航栏的实现。
(2)实现照片栏展示手机存储中的所有图片
要展示手机中存储的所有图片,首先我们需要成功获取到手机中所有图片的路径,通过图片路径访问图片并把图片加载出来。这个功能的实现主要分为以下几个步骤:
动态申请读取存储权限
public static void verifyStoragePermissions(Activity activity) { try { //检测是否有写的权限 int permission = ActivityCompat.checkSelfPermission(activity, "android.permission.WRITE_EXTERNAL_STORAGE"); if (permission != PackageManager.PERMISSION_GRANTED) { // 没有写的权限,去申请写的权限,会弹出对话框 ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,REQUEST_EXTERNAL_STORAGE); } }catch (Exception e) { e.printStackTrace(); } }
读取手机存储查找所有图片并存储图片路径以及其他相关信息。
首先,我定义了一个数据类SpacePhoto用于存储每张图片的相关信息(路径,名称等),SpacePhoto数据类实现了Parcelable类,Parcelable用来从一个组件传输高性能数据到另一个组件,在这里,我们将图片的URL从相册的缩略图界面传递至SpacePhotoActivity。SpacePhoto数据类定义如下:
public class SpacePhoto implements Parcelable { private String mUrl; private String mTitle; public SpacePhoto(String url, String title) { mUrl = url; mTitle = title; } protected SpacePhoto(Parcel in) { mUrl = in.readString(); mTitle = in.readString(); } public static final Creator CREATOR = new Creator() { @Override public SpacePhoto createFromParcel(Parcel in) { return new SpacePhoto(in); } @Override public SpacePhoto[] newArray(int size) { return new SpacePhoto[size]; } }; public String getUrl() { return mUrl; } public void setUrl(String url) { mUrl = url; } public String getTitle() { return mTitle; } public void setTitle(String title) { mTitle = title; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(mUrl); parcel.writeString(mTitle); } }
然后是使用ContentResolver组件查询手机的所有图片,代码如下:
Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; String[] projImage = { MediaStore.Images.Media._ID , MediaStore.Images.Media.DATA ,MediaStore.Images.Media.SIZE ,MediaStore.Images.Media.DISPLAY_NAME}; Cursor mCursor = getActivity().getContentResolver().query(mImageUri, projImage, MediaStore.Images.Media.MIME_TYPE + "=? or " + MediaStore.Images.Media.MIME_TYPE + "=?", new String[]{"image/jpeg", "image/png"}, MediaStore.Images.Media.DATE_MODIFIED+" desc"); if( mCursor != null ) { while(mCursor.moveToNext()){ String path = mCursor.getString(mCursor.getColumnIndex(MediaStore.Images.Media.DATA)); String displayName = mCursor.getString(mCursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)); all_photo_set.add(new SpacePhoto(path,displayName)); //将获取到的路径加入路径集合
注意到一个问题,由于相册的初始页面就是“照片”或者“分类”,所以每次打开APP的时候必须都能够顺利读取手机存储中的图片,即每次打开APP时,都必须保证已获取读取手机存储的权限。但是这样就会面临一个问题,当一部手机第一次安装此APP时,APP还没有获取到读取手机存储的权限(权限需要动态获取并由手机用户决定是否允许读取),则此时相册的布局是无法加载出来的,会直接闪退。为解决此问题,在APP中设置一个引导页,引导页会在APP第一次被打开时出现(以后便不会再出现),询问用户是否给予读取手机存储的权限,进而在进入相册的主页面。另外,在引导页还会完成对手机存储扫描的全过程,获取到手机中的所有图片路径,引导页代码如下:
public class SplashActivity extends AppCompatActivity { private static final int REQUEST_EXTERNAL_STORAGE = 1; private static String[] PERMISSIONS_STORAGE = { "android.permission.READ_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE" }; private MyApplication app; private ArrayList all_photo_set = new ArrayList<>(); // 存放所有图片的路径 private ArrayList all_album = new ArrayList<>(); //按系统相册所属分开照片 public static int MODE = Context.MODE_PRIVATE; private boolean isFirstIn; private SharedPreferences preferences; private Button enter_button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); if (getSupportActionBar() != null){ // 去掉标题栏 getSupportActionBar().hide(); } preferences = getSharedPreferences("first_pref", MODE_PRIVATE); isFirstIn = preferences.getBoolean("isFirstIn", true); if(isFirstIn){ enter_button = findViewById(R.id.enter_button); enter_button.setVisibility(View.VISIBLE); verifyStoragePermissions(SplashActivity.this); SharedPreferences preferences = getSharedPreferences("first_pref", MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.putBoolean("isFirstIn", false); editor.commit(); } else{ enter_button = findViewById(R.id.enter_button); enter_button.setVisibility(View.VISIBLE); startActivity(new Intent(this,MainActivity.class)); finish(); } } public void search_all_picture() { // 获取系统中所有图片的路径 Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; String[] projImage = { MediaStore.Images.Media._ID , MediaStore.Images.Media.DATA ,MediaStore.Images.Media.SIZE ,MediaStore.Images.Media.DISPLAY_NAME}; Cursor mCursor = this.getContentResolver().query(mImageUri, projImage, MediaStore.Images.Media.MIME_TYPE + "=? or " + MediaStore.Images.Media.MIME_TYPE + "=?", new String[]{"image/jpeg", "image/png"}, MediaStore.Images.Media.DATE_MODIFIED+" desc"); if( mCursor != null ) { while(mCursor.moveToNext()){ String path = mCursor.getString(mCursor.getColumnIndex(MediaStore.Images.Media.DATA)); String displayName = mCursor.getString(mCursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)); all_photo_set.add(new SpacePhoto(path,displayName)); //将获取到的路径加入路径集合 String dirPath = new File(path).getParentFile().getAbsolutePath(); // 获取该图片的父路径名 int pdf = 0; //判断此图片所属的相册是否已存在,pdf为0表示图片所属的相册还不存在 for(int i = 0; i < all_album.size(); i++){ if( all_album.get(i).getDirpath().equals(dirPath)){ pdf = 1; all_album.get(i).add(new SpacePhoto(path,displayName)); } } if( pdf == 0){ ArrayList new_list = new ArrayList<>(); new_list.add(new SpacePhoto(path,displayName)); String Str[] = dirPath.split("/"); String album_name = Str[Str.length - 1]; all_album.add(new album(new_list,dirPath,album_name)); } } } } public static void verifyStoragePermissions(Activity activity) { try { //检测是否有写的权限 int permission = ActivityCompat.checkSelfPermission(activity, "android.permission.WRITE_EXTERNAL_STORAGE"); if (permission != PackageManager.PERMISSION_GRANTED) { // 没有写的权限,去申请写的权限,会弹出对话框 ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,REQUEST_EXTERNAL_STORAGE); } }catch (Exception e) { e.printStackTrace(); } } public void gotoMainAct(View view){ search_all_picture(); app = (MyApplication)getMyApplication(); app.set_all_album(all_album); app.set_all_photo_set(all_photo_set); startActivity(new Intent(this,MainActivity.class)); finish(); } }
这里我使用SharedPreferences来处理判断用户是否第一次打开此APP的逻辑。
成功获取图片的路径集合后,根据路径加载图片将所有图片展示出来
实现图片展示列表
使用android提供的初始接口来根据图片url加载图片是比较麻烦的,首先要根据url获取图片的真实路径,再根据真实路径获取bitmap数据类型的图片,最后再通过imageview控件展示出来;其次还有图片加载的性能问题,因为图片展示列表要求快速加载系统中的所有图片。针对以上问题,我使用了一个第三方的Android开源库Glide。Glide是一个快速高效的Android图片加载库,注重于平滑的滚动。Glide提供了易用的API,高性能、可扩展的图片解码管道(decode pipeline),以及自动的资源池技术。Glide 支持拉取,解码和展示视频快照,图片,和GIF动画。Glide的具体使用方法如下:
在build.gradle文件中添加以下依赖:
// Glide compile 'com.github.bumptech.glide:glide:3.7.0' 1 2 通过图片url加载图片 Glide.with(mContext) //传递上下文 .load(spacePhoto.getUrl()) // 目录路径或者URI或者URL .centerCrop() // 图片有可能被裁剪 .placeholder(R.drawable.error) //一个本地APP资源id,在图片被加载前作为占位的图片 .into(imageView); // 要放置图片的目标imageView控件
使用Recyclerview来制作图片展示列表,Recyclerview具体用法如下:
在build.gradle文件中添加以下依赖:
// Recyclerview compile 'com.android.support:recyclerview-v7:25.1.1'
自定义ImageGalleryAdapter,要求继承RecyclerView.Adapter类,具体代码如下:
class ImageGalleryAdapter extends RecyclerView.Adapter { private ArrayList mSpacePhotos; private Context mContext; @Override public ImageGalleryAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { Context context = parent.getContext(); LayoutInflater inflater = LayoutInflater.from(context); View photoView = inflater.inflate(R.layout.item_photo, parent, false); ImageGalleryAdapter.MyViewHolder viewHolder = new ImageGalleryAdapter.MyViewHolder(photoView); return viewHolder; } @Override public void onBindViewHolder(ImageGalleryAdapter.MyViewHolder holder, int position) { SpacePhoto spacePhoto = mSpacePhotos.get(position); ImageView imageView = holder.mPhotoImageView; Glide.with(mContext) //传递上下文 .load(spacePhoto.getUrl()) // 目录路径或者URI或者URL .centerCrop() // 图片有可能被裁剪 .placeholder(R.drawable.error) //一个本地APP资源id,在图片被加载前作为占位的图片 .into(imageView); // 要放置图片的目标imageView控件 } @Override public int getItemCount() { return (mSpacePhotos.size()); } public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { public ImageView mPhotoImageView; public MyViewHolder(View itemView) { super(itemView); mPhotoImageView = (ImageView) itemView.findViewById(R.id.iv_photo); itemView.setOnClickListener(this); } @Override public void onClick(View view) { int position = getAdapterPosition(); if(position != RecyclerView.NO_POSITION) { SpacePhoto spacePhoto = mSpacePhotos.get(position); String url = spacePhoto.getUrl(); Intent intent = new Intent(mContext, SpacePhotoActivity.class); Bundle bundle = new Bundle(); bundle.putString("url",url); intent.putExtras(bundle); mContext.startActivity(intent); } } } public ImageGalleryAdapter(Context context, ArrayList spacePhotos) { mSpacePhotos = new ArrayList<>(); mContext = context; mSpacePhotos = spacePhotos; } }
在fragement1显示图片展示列表,需要在PageFragment1.java文件的onCreateView函数返回一个View,该View加载的是fragment_page1 Laytout;并且,需要在onCreateView函数里设置Recyclerview的Adapter,具体代码如下:
@Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, // 系统会在Fragment首次绘制其用户界面时调用此方法 @Nullable Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_page1,container,false); search_all_picture(); app = (MyApplication)getMyApplication(); app.set_all_photo_set(all_photo_set); //放到Application中 app.set_all_album(all_album); RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(), 2); RecyclerView recyclerView = (RecyclerView) root.findViewById(R.id.rv_images_1); recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(layoutManager); ImageGalleryAdapter adapter = new ImageGalleryAdapter(getActivity(), all_photo_set); //调用这个函数的时候SpacePhoto并不是空的 recyclerView.setAdapter(adapter); return root; }
fragment_page1.xml布局文件如下:
单击图片进入图片展示页面
我使用SpacePhotoActivity用于实现图片展示页面,在ImageGalleryAdapter类中定义内部类MyViewHolder实现View.OnClickListener,并重载onclick函数如下:
@Override public void onClick(View view) { int position = getAdapterPosition(); if(position != RecyclerView.NO_POSITION) { SpacePhoto spacePhoto = mSpacePhotos.get(position); String url = spacePhoto.getUrl(); Intent intent = new Intent(mContext, SpacePhotoActivity.class); Bundle bundle = new Bundle(); bundle.putString("url",url); intent.putExtras(bundle); mContext.startActivity(intent); } }
每次在图片点击列表单击某个图片,则跳转到SpacePhotoActivity并展示被点击的图片,这里使用Intent中传递的是被点击图片的url,在SpacePhotoActivity中根据传递过来的图片url加载图片,代码如下:
mImageView = (ImageView) findViewById(R.id.image); Intent intent = getIntent(); Bundle bundle = intent.getExtras(); String url = bundle.getString("url"); Glide.with(this) .load(url) .asBitmap() .error(R.drawable.error) .into(mImageView);
到这里我们完成了读取手机中的所有照片并将其展示出来的功能。
(3)将手机的所有图片按不同的相册分类,点击各个相册查看其包含图片
获取相册列表没有找到很好的方法,我们都知道获取手机图片要用到ContentResolver类,但是通过这个类却没有办法获取到相册。所以我采用了一种曲线救国的办法:在获取每张图片的路径的时候,同时获取该图片的父路径,然后把父路径相同的所有图片归结为一个相册内的图片,虽然做法有点笨,但是效果还可以,获取父路径并按其分类图片的代码如下:
String dirPath = new File(path).getParentFile().getAbsolutePath(); // 获取该图片的父路径名 int pdf = 0; //判断此图片所属的相册是否已存在,pdf为0表示图片所属的相册还不存在 for(int i = 0; i < all_album.size(); i++){ if( all_album.get(i).getDirpath().equals(dirPath)){ pdf = 1; all_album.get(i).add(new SpacePhoto(path,displayName)); } } if( pdf == 0){ ArrayList new_list = new ArrayList<>(); new_list.add(new SpacePhoto(path,displayName)); String Str[] = dirPath.split("/"); String album_name = Str[Str.length - 1]; all_album.add(new album(new_list,dirPath,album_name)); }
为了更好地表示每个相册,我写了一个相册数据类album,包含三个属性:
一个用来存放该相册所包含的图片的Arraylist;
相册的名称;
该相册内所有图片的父路径;
album数据类的具体实现如下:
public class album { private ArrayList photo_set; private String album_name; private String dirpath; public album(ArrayList list,String path,String str){ photo_set = list; dirpath = path; album_name = str; } public void add(SpacePhoto item){ photo_set.add(item); } public String getAlbum_name(){ return album_name; } public String getDirpath(){ return dirpath; } public int size(){ return photo_set.size(); } public ArrayList getPhotoList(){ return photo_set; } }
注意,album数据类的album_name属性的值是通过该相册中所有图片的公共父路径来获得的。仔细观察路径的表示方式,我发现获取相册名称可通过使用String.spilt函数分解父路径字符串,获取最后一个’/'之后的子串来表示。另外,我们还需要一个Arraylist来存储所有的album。
相册界面要求展示出出相册列表,所以将图片按照相册分类好之后,还需要通过一个Listview来展示出相册列表,ListView的实现比较简单,这里只贴一下代码:
public class listViewAdapter extends BaseAdapter { private ArrayList list; private Context context; public listViewAdapter(ArrayListlist, Context context) { this.list = list; this.context = context; } @Override public int getCount() { if (list == null) { return 0; } return list.size(); } @Override public long getItemId(int i) { return i; } @Override public Object getItem(int i) { if (list == null) { return null; } return list.get(i); } @Override public View getView(int i, View view, ViewGroup viewGroup) { // 通过inflate的方法加载布局,context需要在使用这个Adapter的Activity中传入。 if( view == null ) { view = LayoutInflater.from(context).inflate(R.layout.album, null); } ImageView image = view.findViewById(R.id.cover); TextView album_name = view.findViewById(R.id.album_name); TextView picture_num = view.findViewById(R.id.picture_num); Glide.with(context) //传递上下文 .load(list.get(i).getPhotoList().get(0).getUrl()) // 目录路径或者URI或者URL .centerCrop() // 图片有可能被裁剪 .placeholder(R.drawable.error) //一个本地APP资源id,在图片被加载前作为占位的图片 .into(image); // 要放置图片的目标imageView控件 picture_num.setText(list.get(i).size() + "张图片"); album_name.setText(list.get(i).getAlbum_name()); return view; // 将这个处理好的view返回 } }
fragment_page2布局文件如下:
PageFragment2.java中为listview设置Adapter如下:
albumAdapter = new listViewAdapter(app.get_all_album(),getActivity()); listView = root.findViewById(R.id.listview); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Intent intent = new Intent(getActivity(),album_show_page.class); Bundle bundle = new Bundle(); bundle.putInt("index",position); intent.putExtras(bundle); startActivity(intent); } }); listView.setAdapter(albumAdapter);
点击listview中的相册可跳转到album_show_page,显示该相册所包含的所有图片,图片列表实现的代码只需复用fragment照片的即可。
到这为止我们也顺利实现了相册栏。
(4)关于照片fragment和相册fragment实现内容的补充——重写Application类
注意到照片栏和相册栏的实现都需要首先获取到手机存储中的所有图片,若在照片栏和相册栏分别执行一次查找手机存储获取图片路径的操作会显得非常冗余;而且当在相册列表中每次点击某个相册时,若采用Intent将整个相册中图片的url作为参数传递,则效率会非常低下。为了只进行一次查找手机存储的操作,很自然地想到可以将图片路径的集合设置为全局变量,这样只要在整个项目的某一处进行了查找手机存储获取了所有图片路径的操作,则之后就不再需要重复此操作。在android里声明全局变量可通过重写Application类来实现。Application和Activity,Service一样是android框架的一个系统组件,当android程序启动时系统会创建一个application对象,用来存储系统的一些信息。通常我们是不需要指定一个Application的,这时系统会自动帮我们创建。打开每一个应用程序的manifest文件,可以看到activity都是包含在application标签之中的。android系统会为每个程序运行时创建一个Application类的对象且仅创建一个,所以Application是单例 (singleton)模式的一个类.且application对象的生命周期是整个程序中最长的,它的生命周期就等于这个程序的生命周期。因为它是全局的单例的,所以在不同的Activity,Service中获得的对象都是同一个对象。因此在android中我们可以避免使用静态变量来存储长久保存的值,而用Application。为了更好的利用Application的这一特性,比如我们需要Application来保存一些静态值,需要自定义继承于Application的类,然后在这个类中定义一个变量来保存。在默认情况下应用系统会自动生成Application 对象,但是如果我们自定义了Application,那就需要告知系统,实例化的时候,是实例化我们自定义的,而非默认的。为了让系统实例化的时候找到,我们必须在manifest中修改application标签属性,代码如下:
android:name=".MyApplication"
其中,最关键的是这一句:android:name=".MyApplication"。
重写的MyApplication类如下:
public class MyApplication extends Application{ private static MyApplication instance; private static ArrayList all_photo_set = new ArrayList<>(); // 存放所有图片的路径 private static ArrayList all_album = new ArrayList<>(); //按系统相册所属分开照片 // 获取Application public static Context getMyApplication() { return instance; } public static ArrayList get_all_photo_set(){ return all_photo_set; } public static ArrayList get_all_album(){ return all_album; } public static void set_all_photo_set(ArrayList copy){ all_photo_set = copy; } public static void set_all_album(ArrayList copy){ all_album = copy; } }
由以上代码可知,MyApplication类中既存放了所有的路径集合,也存放了按照相册分好的路径集合,在任何需要这些数据的地方,只要根据相应的函数进行请求即可。在需要获取全局对象的地方,通过以下代码获取application单例:
app = (MyApplication)getMyApplication();
在相册列表界面,点击相册跳转到该相册的图片展示列表,其中intent传递的不是该相册所有图片的url,而是该album在ArrayList中的下标,在show_album_page Activity中,只要根据下标进行请求数据即可。
相关推荐
0评论