android仿华为系统相册之智能相册开发
一、開(kāi)發(fā)內(nèi)容簡(jiǎn)介
最近課程要求仿照華為系統(tǒng)相冊(cè)做一個(gè)android相冊(cè)客戶(hù)端,我稱(chēng)之為智能相冊(cè)(智能是指其使用了機(jī)器學(xué)習(xí)的人臉識(shí)別、人臉檢測(cè)和分類(lèi)算法)。本著反正實(shí)驗(yàn)報(bào)告寫(xiě)了也是寫(xiě)了的心態(tài),還有自己在寫(xiě)的過(guò)程中搜索資料的時(shí)候發(fā)現(xiàn)其實(shí)好像有關(guān)于相冊(cè)列表獲取這部分內(nèi)容網(wǎng)上講的其實(shí)挺少的,雖然我的辦法并不是多么高明,在這里還是給大家分享一下,希望能對(duì)一些人起到幫助。二、開(kāi)發(fā)內(nèi)容要求
智能相冊(cè)APP參照華為系統(tǒng)相冊(cè)的樣式和功能,主要分為三部分內(nèi)容:- 照片:顯示手機(jī)存儲(chǔ)的所有圖片
- 相冊(cè):將手機(jī)的所有圖片按不同的相冊(cè)分類(lèi),點(diǎn)擊各個(gè)相冊(cè)查看其包含圖片
- 分類(lèi):根據(jù)智能算法對(duì)手機(jī)存儲(chǔ)中的所有圖片進(jìn)行整理與分類(lèi),例如“人像”、“地點(diǎn)”、“事物”等
華為系統(tǒng)相冊(cè)樣式如下:
三、智能相冊(cè)效果展示
四、開(kāi)發(fā)過(guò)程詳述
(1)頂部導(dǎo)航欄的實(shí)現(xiàn)
頂部導(dǎo)航欄借用了github上的一個(gè)開(kāi)源第三方庫(kù)wasabeef/awesome-android-ui實(shí)現(xiàn),選取其中的SmartTabLayout控件,注意這里有andoridx和legacy android support library版本,我選取的是legacy android support library版本,具體實(shí)現(xiàn)如下:到這里就順利完成了頂部導(dǎo)航欄的實(shí)現(xiàn)。
(2)實(shí)現(xiàn)照片欄展示手機(jī)存儲(chǔ)中的所有圖片
要展示手機(jī)中存儲(chǔ)的所有圖片,首先我們需要成功獲取到手機(jī)中所有圖片的路徑,通過(guò)圖片路徑訪問(wèn)圖片并把圖片加載出來(lái)。這個(gè)功能的實(shí)現(xiàn)主要分為以下幾個(gè)步驟:- 首先,我定義了一個(gè)數(shù)據(jù)類(lèi)SpacePhoto用于存儲(chǔ)每張圖片的相關(guān)信息(路徑,名稱(chēng)等),SpacePhoto數(shù)據(jù)類(lèi)實(shí)現(xiàn)了Parcelable類(lèi),Parcelable用來(lái)從一個(gè)組件傳輸高性能數(shù)據(jù)到另一個(gè)組件,在這里,我們將圖片的URL從相冊(cè)的縮略圖界面?zhèn)鬟f至SpacePhotoActivity。SpacePhoto數(shù)據(jù)類(lèi)定義如下: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<SpacePhoto> CREATOR = new Creator<SpacePhoto>() {@Overridepublic SpacePhoto createFromParcel(Parcel in) {return new SpacePhoto(in);}@Overridepublic 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;}@Overridepublic int describeContents() {return 0;}@Overridepublic void writeToParcel(Parcel parcel, int i) {parcel.writeString(mUrl);parcel.writeString(mTitle);} }
- 然后是使用ContentResolver組件查詢(xún)手機(jī)的所有圖片,代碼如下: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)); //將獲取到的路徑加入路徑集合 注意到一個(gè)問(wèn)題,由于相冊(cè)的初始頁(yè)面就是“照片”或者“分類(lèi)”,所以每次打開(kāi)APP的時(shí)候必須都能夠順利讀取手機(jī)存儲(chǔ)中的圖片,即每次打開(kāi)APP時(shí),都必須保證已獲取讀取手機(jī)存儲(chǔ)的權(quán)限。但是這樣就會(huì)面臨一個(gè)問(wèn)題,當(dāng)一部手機(jī)第一次安裝此APP時(shí),APP還沒(méi)有獲取到讀取手機(jī)存儲(chǔ)的權(quán)限(權(quán)限需要?jiǎng)討B(tài)獲取并由手機(jī)用戶(hù)決定是否允許讀取),則此時(shí)相冊(cè)的布局是無(wú)法加載出來(lái)的,會(huì)直接閃退。為解決此問(wèn)題,在APP中設(shè)置一個(gè)引導(dǎo)頁(yè),引導(dǎo)頁(yè)會(huì)在APP第一次被打開(kāi)時(shí)出現(xiàn)(以后便不會(huì)再出現(xiàn)),詢(xún)問(wèn)用戶(hù)是否給予讀取手機(jī)存儲(chǔ)的權(quán)限,進(jìn)而在進(jìn)入相冊(cè)的主頁(yè)面。另外,在引導(dǎo)頁(yè)還會(huì)完成對(duì)手機(jī)存儲(chǔ)掃描的全過(guò)程,獲取到手機(jī)中的所有圖片路徑,引導(dǎo)頁(yè)代碼如下: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<SpacePhoto> all_photo_set = new ArrayList<>(); // 存放所有圖片的路徑 private ArrayList<album> all_album = new ArrayList<>(); //按系統(tǒng)相冊(cè)所屬分開(kāi)照片 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){ // 去掉標(biāo)題欄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() { // 獲取系統(tǒng)中所有圖片的路徑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; //判斷此圖片所屬的相冊(cè)是否已存在,pdf為0表示圖片所屬的相冊(cè)還不存在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<SpacePhoto> 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 {//檢測(cè)是否有寫(xiě)的權(quán)限int permission = ActivityCompat.checkSelfPermission(activity,"android.permission.WRITE_EXTERNAL_STORAGE");if (permission != PackageManager.PERMISSION_GRANTED) {// 沒(méi)有寫(xiě)的權(quán)限,去申請(qǐng)寫(xiě)的權(quán)限,會(huì)彈出對(duì)話(huà)框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來(lái)處理判斷用戶(hù)是否第一次打開(kāi)此APP的邏輯。
- 實(shí)現(xiàn)圖片展示列表
使用android提供的初始接口來(lái)根據(jù)圖片url加載圖片是比較麻煩的,首先要根據(jù)url獲取圖片的真實(shí)路徑,再根據(jù)真實(shí)路徑獲取bitmap數(shù)據(jù)類(lèi)型的圖片,最后再通過(guò)imageview控件展示出來(lái);其次還有圖片加載的性能問(wèn)題,因?yàn)閳D片展示列表要求快速加載系統(tǒng)中的所有圖片。針對(duì)以上問(wèn)題,我使用了一個(gè)第三方的Android開(kāi)源庫(kù)Glide。Glide是一個(gè)快速高效的Android圖片加載庫(kù),注重于平滑的滾動(dòng)。Glide提供了易用的API,高性能、可擴(kuò)展的圖片解碼管道(decode pipeline),以及自動(dòng)的資源池技術(shù)。Glide 支持拉取,解碼和展示視頻快照,圖片,和GIF動(dòng)畫(huà)。Glide的具體使用方法如下:- 在build.gradle文件中添加以下依賴(lài):
- 通過(guò)圖片url加載圖片
- 在build.gradle文件中添加以下依賴(lài):
- 自定義ImageGalleryAdapter,要求繼承RecyclerView.Adapter類(lèi),具體代碼如下:
- 單擊圖片進(jìn)入圖片展示頁(yè)面
我使用SpacePhotoActivity用于實(shí)現(xiàn)圖片展示頁(yè)面,在ImageGalleryAdapter類(lèi)中定義內(nèi)部類(lèi)MyViewHolder實(shí)現(xiàn)View.OnClickListener,并重載onclick函數(shù)如下:@Overridepublic 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);}} 每次在圖片點(diǎn)擊列表單擊某個(gè)圖片,則跳轉(zhuǎn)到SpacePhotoActivity并展示被點(diǎn)擊的圖片,這里使用Intent中傳遞的是被點(diǎn)擊圖片的url,在SpacePhotoActivity中根據(jù)傳遞過(guò)來(lái)的圖片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);
到這里我們完成了讀取手機(jī)中的所有照片并將其展示出來(lái)的功能。
(3)將手機(jī)的所有圖片按不同的相冊(cè)分類(lèi),點(diǎn)擊各個(gè)相冊(cè)查看其包含圖片
-
獲取相冊(cè)列表沒(méi)有找到很好的方法,我們都知道獲取手機(jī)圖片要用到ContentResolver類(lèi),但是通過(guò)這個(gè)類(lèi)卻沒(méi)有辦法獲取到相冊(cè)。所以我采用了一種曲線救國(guó)的辦法:在獲取每張圖片的路徑的時(shí)候,同時(shí)獲取該圖片的父路徑,然后把父路徑相同的所有圖片歸結(jié)為一個(gè)相冊(cè)內(nèi)的圖片,雖然做法有點(diǎn)笨,但是效果還可以,獲取父路徑并按其分類(lèi)圖片的代碼如下:
String dirPath = new File(path).getParentFile().getAbsolutePath(); // 獲取該圖片的父路徑名 int pdf = 0; //判斷此圖片所屬的相冊(cè)是否已存在,pdf為0表示圖片所屬的相冊(cè)還不存在 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<SpacePhoto> 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)); }為了更好地表示每個(gè)相冊(cè),我寫(xiě)了一個(gè)相冊(cè)數(shù)據(jù)類(lèi)album,包含三個(gè)屬性:
- 一個(gè)用來(lái)存放該相冊(cè)所包含的圖片的Arraylist;
- 相冊(cè)的名稱(chēng);
- 該相冊(cè)內(nèi)所有圖片的父路徑;
album數(shù)據(jù)類(lèi)的具體實(shí)現(xiàn)如下:
public class album { private ArrayList<SpacePhoto> photo_set; private String album_name; private String dirpath;public album(ArrayList<SpacePhoto> 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<SpacePhoto> getPhotoList(){return photo_set; } }注意,album數(shù)據(jù)類(lèi)的album_name屬性的值是通過(guò)該相冊(cè)中所有圖片的公共父路徑來(lái)獲得的。仔細(xì)觀察路徑的表示方式,我發(fā)現(xiàn)獲取相冊(cè)名稱(chēng)可通過(guò)使用String.spilt函數(shù)分解父路徑字符串,獲取最后一個(gè)’/'之后的子串來(lái)表示。另外,我們還需要一個(gè)Arraylist來(lái)存儲(chǔ)所有的album。
-
相冊(cè)界面要求展示出出相冊(cè)列表,所以將圖片按照相冊(cè)分類(lèi)好之后,還需要通過(guò)一個(gè)Listview來(lái)展示出相冊(cè)列表,ListView的實(shí)現(xiàn)比較簡(jiǎn)單,這里只貼一下代碼:
public class listViewAdapter extends BaseAdapter {private ArrayList<album> list;private Context context;public listViewAdapter(ArrayList<album>list, Context context){this.list = list;this.context = context;}@Overridepublic int getCount() {if (list == null) {return 0;}return list.size();}@Overridepublic long getItemId(int i) {return i;}@Overridepublic Object getItem(int i) {if (list == null) {return null;}return list.get(i);}@Overridepublic View getView(int i, View view, ViewGroup viewGroup) {// 通過(guò)inflate的方法加載布局,context需要在使用這個(gè)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) //一個(gè)本地APP資源id,在圖片被加載前作為占位的圖片.into(image); // 要放置圖片的目標(biāo)imageView控件picture_num.setText(list.get(i).size() + "張圖片");album_name.setText(list.get(i).getAlbum_name());return view; // 將這個(gè)處理好的view返回} }fragment_page2布局文件如下:
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><ListViewandroid:id="@+id/listview"android:layout_width="match_parent"android:layout_height="match_parent"></ListView></android.support.design.widget.CoordinatorLayout>PageFragment2.java中為listview設(shè)置Adapter如下:
albumAdapter = new listViewAdapter(app.get_all_album(),getActivity());listView = root.findViewById(R.id.listview);listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic 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);點(diǎn)擊listview中的相冊(cè)可跳轉(zhuǎn)到album_show_page,顯示該相冊(cè)所包含的所有圖片,圖片列表實(shí)現(xiàn)的代碼只需復(fù)用fragment照片的即可。
到這為止我們也順利實(shí)現(xiàn)了相冊(cè)欄。
(4)關(guān)于照片fragment和相冊(cè)fragment實(shí)現(xiàn)內(nèi)容的補(bǔ)充——重寫(xiě)Application類(lèi)
注意到照片欄和相冊(cè)欄的實(shí)現(xiàn)都需要首先獲取到手機(jī)存儲(chǔ)中的所有圖片,若在照片欄和相冊(cè)欄分別執(zhí)行一次查找手機(jī)存儲(chǔ)獲取圖片路徑的操作會(huì)顯得非常冗余;而且當(dāng)在相冊(cè)列表中每次點(diǎn)擊某個(gè)相冊(cè)時(shí),若采用Intent將整個(gè)相冊(cè)中圖片的url作為參數(shù)傳遞,則效率會(huì)非常低下。為了只進(jìn)行一次查找手機(jī)存儲(chǔ)的操作,很自然地想到可以將圖片路徑的集合設(shè)置為全局變量,這樣只要在整個(gè)項(xiàng)目的某一處進(jìn)行了查找手機(jī)存儲(chǔ)獲取了所有圖片路徑的操作,則之后就不再需要重復(fù)此操作。在android里聲明全局變量可通過(guò)重寫(xiě)Application類(lèi)來(lái)實(shí)現(xiàn)。Application和Activity,Service一樣是android框架的一個(gè)系統(tǒng)組件,當(dāng)android程序啟動(dòng)時(shí)系統(tǒng)會(huì)創(chuàng)建一個(gè)application對(duì)象,用來(lái)存儲(chǔ)系統(tǒng)的一些信息。通常我們是不需要指定一個(gè)Application的,這時(shí)系統(tǒng)會(huì)自動(dòng)幫我們創(chuàng)建。打開(kāi)每一個(gè)應(yīng)用程序的manifest文件,可以看到activity都是包含在application標(biāo)簽之中的。android系統(tǒng)會(huì)為每個(gè)程序運(yùn)行時(shí)創(chuàng)建一個(gè)Application類(lèi)的對(duì)象且僅創(chuàng)建一個(gè),所以Application是單例 (singleton)模式的一個(gè)類(lèi).且application對(duì)象的生命周期是整個(gè)程序中最長(zhǎng)的,它的生命周期就等于這個(gè)程序的生命周期。因?yàn)樗侨值膯卫?#xff0c;所以在不同的Activity,Service中獲得的對(duì)象都是同一個(gè)對(duì)象。因此在android中我們可以避免使用靜態(tài)變量來(lái)存儲(chǔ)長(zhǎng)久保存的值,而用Application。為了更好的利用Application的這一特性,比如我們需要Application來(lái)保存一些靜態(tài)值,需要自定義繼承于Application的類(lèi),然后在這個(gè)類(lèi)中定義一個(gè)變量來(lái)保存。在默認(rèn)情況下應(yīng)用系統(tǒng)會(huì)自動(dòng)生成Application 對(duì)象,但是如果我們自定義了Application,那就需要告知系統(tǒng),實(shí)例化的時(shí)候,是實(shí)例化我們自定義的,而非默認(rèn)的。為了讓系統(tǒng)實(shí)例化的時(shí)候找到,我們必須在manifest中修改application標(biāo)簽屬性,代碼如下: <applicationandroid:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/AppTheme">android:name=".MyApplication"<activity android:name=".MainActivity"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><activityandroid:name=".SpacePhotoActivity" /><activity android:name=".album_show_page"></activity></application>其中,最關(guān)鍵的是這一句:android:name=".MyApplication"。
重寫(xiě)的MyApplication類(lèi)如下:
由以上代碼可知,MyApplication類(lèi)中既存放了所有的路徑集合,也存放了按照相冊(cè)分好的路徑集合,在任何需要這些數(shù)據(jù)的地方,只要根據(jù)相應(yīng)的函數(shù)進(jìn)行請(qǐng)求即可。在需要獲取全局對(duì)象的地方,通過(guò)以下代碼獲取application單例:
app = (MyApplication)getMyApplication();在相冊(cè)列表界面,點(diǎn)擊相冊(cè)跳轉(zhuǎn)到該相冊(cè)的圖片展示列表,其中intent傳遞的不是該相冊(cè)所有圖片的url,而是該album在ArrayList中的下標(biāo),在show_album_page Activity中,只要根據(jù)下標(biāo)進(jìn)行請(qǐng)求數(shù)據(jù)即可。
關(guān)于分類(lèi)欄的分類(lèi)算法,由于涉及科研隱私,在這里就不說(shuō)了,文章很長(zhǎng),謝謝閱讀。
總結(jié)
以上是生活随笔為你收集整理的android仿华为系统相册之智能相册开发的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 计算机辅助项目管理实验论文,计算机辅助项
- 下一篇: 汇编实验五zxt