Xamarin : Android : FilePicker 檔案瀏覽
其實這是用來搞懂 這一篇 Browse Files - Xamarin
範例則來自 GitHub 的 mgmclemore 收集的 A collection of Xamarin.Android sample projects
由於 android 瀏覽檔案時沒有像 Windows 一般有『現成』的檔案瀏覽介面(SHELL)可以使用,所以就非常麻煩的必須自己去作出像檔案瀏覽器一樣的介面。
而且 Xamarin 開發的介面中也沒有可以直接使用的元件,所以這完全必須依賴外部套件才有辦法作出來。
這個套件引用了 Xamarin.Android.Support.v4 這個套件,所以必須先到NuGet去下載這個套件到專案內。
它的概念大概就是這樣的:
在畫面上崁入一個 fragment 的自訂類別 FileListFragment 在 FileListFragment 裡面有個 FileListAdapter 這個 adapter ,利用這個 adapter 進行檔案系統的讀取,當 adapter 讀取檔案/資料夾後再使用 FileListRowViewHolder 把 file_picker_list_item.axml 定義的樣式 安插到 fragmant 畫面裡面....bababa 諸如此類。
然而,原本 GitHub 的範例有很明顯的問題,就是:
1、沒有返回前一個目錄的功能,只能一直進入,然後在系統按下 Home 回到主畫面,在重新叫回畫面時,資料夾又回到最原始顯示(Root)。
2、只能固定讀取內部記憶體空間,無法讀取外部擴充記憶卡內容。
所以,我便對這個範例進行了改造,增加了3個按鈕,讓它可以選擇讀取 內部/外部 的記憶體位置,以及可以返回前一路徑的按鈕
我的程式範例下載 (42MB)
程式碼:
main.axml
注意上面粗體字的那一段就是使用自訂類別
file_picker_list_item.axml(自訂列表插件樣式)
粉色是圖片,藍色是文字
FilePickerActivity.cs(主畫面)
此 Activity 繼承自 Android.Support.V4.App.FragmentActivity
FileListFragment.cs(自定類別fragment)
FileListAdapter.cs (用來讀取檔案列表的adapter)
FileListRowViewHolder.cs (顯示列項用的物件)
範例則來自 GitHub 的 mgmclemore 收集的 A collection of Xamarin.Android sample projects
由於 android 瀏覽檔案時沒有像 Windows 一般有『現成』的檔案瀏覽介面(SHELL)可以使用,所以就非常麻煩的必須自己去作出像檔案瀏覽器一樣的介面。
而且 Xamarin 開發的介面中也沒有可以直接使用的元件,所以這完全必須依賴外部套件才有辦法作出來。
這個套件引用了 Xamarin.Android.Support.v4 這個套件,所以必須先到NuGet去下載這個套件到專案內。
它的概念大概就是這樣的:
在畫面上崁入一個 fragment 的自訂類別 FileListFragment 在 FileListFragment 裡面有個 FileListAdapter 這個 adapter ,利用這個 adapter 進行檔案系統的讀取,當 adapter 讀取檔案/資料夾後再使用 FileListRowViewHolder 把 file_picker_list_item.axml 定義的樣式 安插到 fragmant 畫面裡面....bababa 諸如此類。
然而,原本 GitHub 的範例有很明顯的問題,就是:
1、沒有返回前一個目錄的功能,只能一直進入,然後在系統按下 Home 回到主畫面,在重新叫回畫面時,資料夾又回到最原始顯示(Root)。
2、只能固定讀取內部記憶體空間,無法讀取外部擴充記憶卡內容。
所以,我便對這個範例進行了改造,增加了3個按鈕,讓它可以選擇讀取 內部/外部 的記憶體位置,以及可以返回前一路徑的按鈕
我的程式範例下載 (42MB)
程式碼:
main.axml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/linearLayout1">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/linearLayout2">
<Button
android:text="內部空間"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/button1" />
<Button
android:text="MicroSD"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/button2"
android:textAllCaps="false" />
</LinearLayout>
<Button
android:text="/sdcard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/button3"
android:textAllCaps="false"
android:gravity="fill_vertical" />
<fragment
class="com.xamarin.recipes.filepicker.FileListFragment"
android:id="@+id/file_list_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</FrameLayout>
注意上面粗體字的那一段就是使用自訂類別
file_picker_list_item.axml(自訂列表插件樣式)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/file_picker_image"
android:layout_width="40dip"
android:layout_height="40dip"
android:layout_marginTop="5dip"
android:layout_marginBottom="5dip"
android:layout_marginLeft="5dip"
android:src="@drawable/file"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/file_picker_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="left|center_vertical"
android:textSize="28sp"
android:layout_marginLeft="10dip"
android:singleLine="true"
android:text="filename" />
</LinearLayout>
粉色是圖片,藍色是文字
FilePickerActivity.cs(主畫面)
此 Activity 繼承自 Android.Support.V4.App.FragmentActivity
namespace com.xamarin.recipes.filepicker
{
using Android.App;
using Android.OS;
using Android.Support.V4.App;
using Android.Widget;
[Activity(Label = "@string/app_name", MainLauncher = true, Icon = "@drawable/ic_launcher")]
public class FilePickerActivity : FragmentActivity
{
//記錄FileListFragment回傳的路徑
private string locLabel;
//記錄FileListFragment.OnListItemClick按下檔案後的回傳
private string selectedFile;
//提供給FileListFragment回寫的屬性,並觸發相關的動作
public string LocLabel {
get { return locLabel; }
set {
locLabel = value;
updateLabel(locLabel);
}
}
public string SelectedFile
{
set
{
selectedFile = value;
//底下可以處理使用者選擇檔案以後的動作
Toast.MakeText(this, "選擇的檔案: " + selectedFile, ToastLength.Long).Show();
//return selected file back to previous activity
}
}
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.main);
var intrkBtn = FindViewById<Button>(Resource.Id.button1);
intrkBtn.Click += delegate
{
setInternal();
};
var extrkBtn = FindViewById<Button>(Resource.Id.button2);
extrkBtn.Click += delegate
{
setExternal();
};
var backBtn = FindViewById<Button>(Resource.Id.button3);
backBtn.Click += goBackDir;
setInternal();
}
/// <summary>
/// 返回前一個目錄按鈕,此按鈕文字會顯示目前路徑
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void goBackDir(object sender, System.EventArgs e)
{
int slashPos;
string sPath;
//此處注意locLabel必須初始化不然會有exception
slashPos = locLabel.LastIndexOf("/");
if (slashPos == 0)
{
Toast.MakeText(this, "已經到達根目錄", ToastLength.Long).Show();
return;
}
//去除最後一個路徑名稱
sPath = locLabel.Substring(0, slashPos);
locLabel = sPath;
//更新返回路徑按紐文字
updateLabel(sPath);
//更新 FileListFragment 畫面
FileListFragment fragment = (FileListFragment)SupportFragmentManager.FindFragmentById(Resource.Id.file_list_fragment);
fragment.RefreshFilesList(sPath);
}
/// <summary>
/// 設定為內部記憶空間為 FileListFragment 檢索目標
/// </summary>
private void setInternal()
{
locLabel = "/sdcard";
updateLabel(locLabel);
FileListFragment fragment = (FileListFragment)SupportFragmentManager.FindFragmentById(Resource.Id.file_list_fragment);
fragment.RefreshFilesList(locLabel);
}
/// <summary>
/// 設定為外部記憶空間為 FileListFragment 檢索目標
/// </summary>
private void setExternal()
{
locLabel = "/storage";
updateLabel(locLabel);
FileListFragment fragment = (FileListFragment)SupportFragmentManager.FindFragmentById(Resource.Id.file_list_fragment);
fragment.RefreshFilesList(locLabel);
}
/// <summary>
/// 更新返回前一目錄按紐文字
/// </summary>
/// <param name="pathText">完整路徑</param>
private void updateLabel(string pathText)
{
var backBtn = FindViewById<Button>(Resource.Id.button3);
//限制顯示文字長度避免超過按鈕長度,若超過長度則以最後2個路徑名稱顯示
if (pathText.Length > 40)
{
string[] arrayPath = pathText.Substring(1).Split('/');
string newTxt = "/.../" + arrayPath[arrayPath.Length - 2] + "/" + arrayPath[arrayPath.Length - 1];
//若最後兩個路徑名稱長度還是超過限制,則只顯示最後一個路徑
if (newTxt.Length > 40)
{
backBtn.Text = "/../../" + arrayPath[arrayPath.Length - 1];
}
else
{
backBtn.Text = "/.../" + arrayPath[arrayPath.Length - 2] + "/" + arrayPath[arrayPath.Length - 1];
}
}
else
{
backBtn.Text = pathText;
}
}
}
}
FileListFragment.cs(自定類別fragment)
namespace com.xamarin.recipes.filepicker
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Android.OS;
using Android.Support.V4.App;
using Android.Util;
using Android.Views;
using Android.Widget;
/// <summary>
/// A ListFragment that will show the files and subdirectories of a given directory.
/// </summary>
/// <remarks>
/// <para> This was placed into a ListFragment to make this easier to share this functionality with with tablets. </para>
/// <para> Note that this is a incomplete example. It lacks things such as the ability to go back up the directory tree, or any special handling of a file when it is selected. </para>
/// </remarks>
public class FileListFragment : ListFragment
{
public static string DefaultInitialDirectory = "/sdcard";
private FileListAdapter _adapter;
private DirectoryInfo _directory;
public string currentLocation
{
get { return _directory.FullName; }
}
public string DefaultDirectory
{
set { DefaultInitialDirectory = value; }
get { return DefaultInitialDirectory; }
}
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
_adapter = new FileListAdapter(Activity, new FileSystemInfo[0]);
ListAdapter = _adapter;
}
public override void OnListItemClick(ListView l, View v, int position, long id)
{
var fileSystemInfo = _adapter.GetItem(position);
if (fileSystemInfo.IsFile())
{
// Do something with the file. In this case we just pop some toast.
Log.Verbose("FileListFragment", "file {0} was clicked.", fileSystemInfo.FullName);
//Toast.MakeText(Activity, "選擇的檔案: " + fileSystemInfo.FullName, ToastLength.Short).Show();
//將選擇檔案傳遞變更給呼叫的Activity屬性
((FilePickerActivity)this.Activity).SelectedFile = fileSystemInfo.FullName;
}
else
{
//從這裡觸發呼叫的Activity,並變更其屬性紀錄
((FilePickerActivity)this.Activity).LocLabel = fileSystemInfo.FullName;
// Dig into this directory, and display it's contents
RefreshFilesList(fileSystemInfo.FullName);
}
base.OnListItemClick(l, v, position, id);
}
public override void OnResume()
{
base.OnResume();
//this first retrieve from activity
//RefreshFilesList(DefaultInitialDirectory);
}
public void RefreshFilesList(string directory)
{
IList<FileSystemInfo> visibleThings = new List<FileSystemInfo>();
var dir = new DirectoryInfo(directory);
try
{
foreach (var item in dir.GetFileSystemInfos().Where(item => item.IsVisible()))
{
visibleThings.Add(item);
}
}
catch (Exception ex)
{
Log.Error("FileListFragment", "Can't retriving directory : " + dir.FullName + "; " + ex);
Toast.MakeText(Activity, "讀取下面路徑發生問題: " + directory, ToastLength.Long).Show();
return;
}
_directory = dir;
_adapter.AddDirectoryContents(visibleThings);
// If we don't do this, then the ListView will not update itself when then data set
// in the adapter changes. It will appear to the user that nothing has happened.
ListView.RefreshDrawableState();
Log.Verbose("FileListFragment", "Displaying directory : {0}.", directory);
}
}
}
FileListAdapter.cs (用來讀取檔案列表的adapter)
namespace com.xamarin.recipes.filepicker
{
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Android.Content;
using Android.Views;
using Android.Widget;
public class FileListAdapter : ArrayAdapter<FileSystemInfo>
{
private readonly Context _context;
public FileListAdapter(Context context, IList<FileSystemInfo> fsi)
: base(context, Resource.Layout.file_picker_list_item, Android.Resource.Id.Text1, fsi)
{
_context = context;
}
/// <summary>
/// We provide this method to get around some of the
/// </summary>
/// <param name="directoryContents"> </param>
public void AddDirectoryContents(IEnumerable<FileSystemInfo> directoryContents)
{
Clear();
// Notify the _adapter that things have changed or that there is nothing
// to display.
if (directoryContents.Any())
{
#if __ANDROID_11__
// .AddAll was only introduced in API level 11 (Android 3.0).
// If the "Minimum Android to Target" is set to Android 3.0 or
// higher, then this code will be used.
AddAll(directoryContents.ToArray());
#else
// This is the code to use if the "Minimum Android to Target" is
// set to a pre-Android 3.0 API (i.e. Android 2.3.3 or lower).
lock (this)
foreach (var fsi in directoryContents)
{
Add(fsi);
}
#endif
NotifyDataSetChanged();
}
else
{
NotifyDataSetInvalidated();
}
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
var fileSystemEntry = GetItem(position);
FileListRowViewHolder viewHolder;
View row;
if (convertView == null)
{
row = _context.GetLayoutInflater().Inflate(Resource.Layout.file_picker_list_item, parent, false);
viewHolder = new FileListRowViewHolder(row.FindViewById<TextView>(Resource.Id.file_picker_text), row.FindViewById<ImageView>(Resource.Id.file_picker_image));
row.Tag = viewHolder;
}
else
{
row = convertView;
viewHolder = (FileListRowViewHolder)row.Tag;
}
viewHolder.Update(fileSystemEntry.Name, fileSystemEntry.IsDirectory() ? Resource.Drawable.folder : Resource.Drawable.file);
return row;
}
}
}
FileListRowViewHolder.cs (顯示列項用的物件)
namespace com.xamarin.recipes.filepicker
{
using Android.Widget;
using Java.Lang;
/// <summary>
/// This class is used to hold references to the views contained in a list row.
/// </summary>
/// <remarks>
/// This is an optimization so that we don't have to always look up the
/// ImageView and the TextView for a given row in the ListView.
/// </remarks>
public class FileListRowViewHolder : Object
{
public FileListRowViewHolder(TextView textView, ImageView imageView)
{
TextView = textView;
ImageView = imageView;
}
public ImageView ImageView { get; private set; }
public TextView TextView { get; private set; }
/// <summary>
/// This method will update the TextView and the ImageView that are
/// are
/// </summary>
/// <param name="fileName"> </param>
/// <param name="fileImageResourceId"> </param>
public void Update(string fileName, int fileImageResourceId)
{
TextView.Text = fileName;
ImageView.SetImageResource(fileImageResourceId);
}
}
}
留言