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

<?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);
        }
    }
}








留言

這個網誌中的熱門文章

【研究】列印的條碼為什麼很難刷(掃描)

C# 使用 Process.Start 執行外部程式

統一發票列印小程式