关于Android文件搜索功能的学习

我们来看一下文件夹管理器Amaze是怎么实现Search操作的。一般来说,文件的操作与处理我们都会大量的使用Java提供的File类。
通过各种File操作,来达到我们的目的。

简单回顾一下AsyncTask

  • onPreExecute()
    后台任务开始执行之间调用
  • doInBackground(Params…)
    在后台执行耗时任务,当前的任务都在子线程中执行
    任务一旦完成就可以通过return语句来将任务的执行结果进行返回,
    如果AsyncTask的第三个泛型参数指定的是Void,就可以不返回任务执行结果。
  • onProgressUpdate(Progress…)
    当在后台任务中调用了publishProgress(Progress…)方法后,这个方法就很快会被调用,
    方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,
    利用参数中的数值就可以对界面元素进行相应的更新。
  • onPostExecute(Result)
    当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。
    返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些UI操作,
    比如说提醒任务执行的结果,以及关闭掉进度条对话框等。

如何初始化搜索功能?

用户点击搜索框并且输入字符这时候做了什么?

//MainActivity中
private AppBar appbar;

 @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //....
    appbar = new AppBar(this, getPrefs(), queue -> {
            if(!queue.isEmpty()) {
                mainActivityHelper.search(getPrefs(), queue);
            }
        });
       //....
}

AppBar可以方便调用Toolbar与BottomBar的实例。所以我们更加关心它的构造方法做了什么。

 public AppBar(MainActivity a, SharedPreferences sharedPref, SearchView.SearchListener searchListener) {
      //....
        searchView = new SearchView(this, a, searchListener);
      //....
    }

在AppBar中实例化了SearhView。在searchView中处理了输入监听,以及搜索结果回调,和搜索框动画。

图片搜索框

public SearchView(final AppBar appbar, final MainActivity a, final SearchListener searchListener) {
//...这里通过SearchListener接口设置了监听。
 searchViewEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                    searchListener.onSearch(searchViewEditText.getText().toString());
                    appbar.getSearchView().hideSearchView();
                    return true;
                }
                return false;
            }   
   });
//...
}

这里顺便把搜索栏显示的动画的代码贴出来

    //点击搜索图标,变成搜索栏
 public void revealSearchView() {
        final int START_RADIUS = 16;
        int endRadius = Math.max(appbar.getToolbar().getWidth(), appbar.getToolbar().getHeight());

        Animator animator;
        //如果SDK大于5.0
        if (SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            int[] searchCoords = new int[2];
            View searchItem = appbar.getToolbar().findViewById(R.id.search);//It could change position, get it every time
            searchViewEditText.setText("");
            searchItem.getLocationOnScreen(searchCoords);
            //使用圆形缩放动画的api
            animator = ViewAnimationUtils.createCircularReveal(searchViewLayout,
                    searchCoords[0] + 32, searchCoords[1] - 16, START_RADIUS, endRadius);
        } else {
            // TODO:ViewAnimationUtils.createCircularReveal
            //使用透明渐变的动画
            animator = ObjectAnimator.ofFloat(searchViewLayout, "alpha", 0f, 1f);
        }

        mainActivity.showSmokeScreen();

        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.setDuration(600);
        searchViewLayout.setVisibility(View.VISIBLE);
        animator.start();
        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {}

            @Override
            public void onAnimationEnd(Animator animation) {
            //动画结束之后,请求键盘弹出
                searchViewEditText.requestFocus();
                InputMethodManager imm = (InputMethodManager) mainActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
                imm.showSoftInput(searchViewEditText, InputMethodManager.SHOW_IMPLICIT);
                enabled = true;
            }

            @Override
            public void onAnimationCancel(Animator animation) {}

            @Override
            public void onAnimationRepeat(Animator animation) {}
        });
    }

    /**
     * 隐藏搜索栏,变成搜索图标
     */
    public void hideSearchView() {
        final int END_RADIUS = 16;
        int startRadius = Math.max(searchViewLayout.getWidth(), searchViewLayout.getHeight());
        Animator animator;
        if (SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            int[] searchCoords = new int[2];
            View searchItem = appbar.getToolbar().findViewById(R.id.search);//It could change position, get it every time
            searchViewEditText.setText("");
            searchItem.getLocationOnScreen(searchCoords);
            animator = ViewAnimationUtils.createCircularReveal(searchViewLayout,
                    searchCoords[0] + 32, searchCoords[1] - 16, startRadius, END_RADIUS);
        } else {
            // TODO: ViewAnimationUtils.createCircularReveal
            animator = ObjectAnimator.ofFloat(searchViewLayout, "alpha", 1f, 0f);
        }

        // removing background fade view
        mainActivity.hideSmokeScreen();
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.setDuration(600);
        animator.start();
        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {}

            @Override
            public void onAnimationEnd(Animator animation) {
                searchViewLayout.setVisibility(View.GONE);
                enabled = false;
                InputMethodManager inputMethodManager = (InputMethodManager) mainActivity.getSystemService(INPUT_METHOD_SERVICE);
                inputMethodManager.hideSoftInputFromWindow(searchViewEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
            }

            @Override
            public void onAnimationCancel(Animator animation) {}

            @Override
            public void onAnimationRepeat(Animator animation) {}
        });
    }

到了这里我们看到了怎么显示出搜索栏,接着看看如何处理搜索数据。

加载Search的Fragment

MainActivityonCreate()方法中有mainActivityHelper.search(getPrefs(), queue);

MainActivityHelper

        //在搜索的过程中还是需要去add一个search的fragment.
public void search(SharedPreferences sharedPrefs, String query) {
        TabFragment tabFragment = mainActivity.getTabFragment();
        if (tabFragment == null) return;
        final MainFragment ma = (MainFragment) tabFragment.getCurrentTabFragment();
        final String fpath = ma.getCurrentPath();


        SEARCH_TEXT = query;
        mainActivity.mainFragment = (MainFragment) mainActivity.getTabFragment().getCurrentTabFragment();
        FragmentManager fm = mainActivity.getSupportFragmentManager();
        SearchWorkerFragment fragment =
                (SearchWorkerFragment) fm.findFragmentByTag(MainActivity.TAG_ASYNC_HELPER);

        if (fragment != null) {
            if (fragment.mSearchAsyncTask.getStatus() == AsyncTask.Status.RUNNING) {
                fragment.mSearchAsyncTask.cancel(true);
            }
            fm.beginTransaction().remove(fragment).commit();
        }
        //添加搜索的fragment,这样好管理。
        addSearchFragment(fm, new SearchWorkerFragment(), fpath, query, ma.openMode, mainActivity.isRootExplorer(),
                sharedPrefs.getBoolean(SearchWorkerFragment.KEY_REGEX, false),
                sharedPrefs.getBoolean(SearchWorkerFragment.KEY_REGEX_MATCHES, false));
    }

 public static void addSearchFragment(FragmentManager fragmentManager, Fragment fragment,
                                         String path, String input, OpenMode openMode, boolean rootMode,
                                         boolean regex, boolean matches) {
        Bundle args = new Bundle();
        //输入的String
        args.putString(SearchWorkerFragment.KEY_INPUT, input);
        //当前的路径
        args.putString(SearchWorkerFragment.KEY_PATH, path);
        //文件模式
        args.putInt(SearchWorkerFragment.KEY_OPEN_MODE, openMode.ordinal());
        //是否是root
        args.putBoolean(SearchWorkerFragment.KEY_ROOT_MODE, rootMode);
        //高级匹配
        args.putBoolean(SearchWorkerFragment.KEY_REGEX, regex);
        //高级正则匹配
        args.putBoolean(SearchWorkerFragment.KEY_REGEX_MATCHES, matches);

        fragment.setArguments(args);
        fragmentManager.beginTransaction().add(fragment, MainActivity.TAG_ASYNC_HELPER).commit();
    }

构建静态有参数的Fragment之后,通过Bundle对象传参数,这样即使旋转屏幕,Fragment被重启,传递的参数也也被调用到。
来到SearchWorkerFragment

public class SearchWorkerFragment extends Fragment {

    //省略.....
    public SearchAsyncTask mSearchAsyncTask;


    private HelperCallbacks mCallbacks;

    // Activity需要实现的接口
    public interface HelperCallbacks {
        void onPreExecute(String query);
        void onPostExecute(String query);
        void onProgressUpdate(HybridFileParcelable val, String query);
        void onCancelled();
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        // 因为设备配置发生变化,所以请保持活动的实例
        mCallbacks = (HelperCallbacks) context;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //该fragment的视图立即被销毁,但fragment本身不会被销毁
        //为适应新的设备配置,当新的Activity创建后,新的FragmentManager会找到被保留的fragment,并重新创建其试图。
        setRetainInstance(true);
        String mPath = getArguments().getString(KEY_PATH);
        String mInput = getArguments().getString(KEY_INPUT);
        OpenMode mOpenMode = OpenMode.getOpenMode(getArguments().getInt(KEY_OPEN_MODE));
        boolean mRootMode = getArguments().getBoolean(KEY_ROOT_MODE);
        boolean isRegexEnabled = getArguments().getBoolean(KEY_REGEX);
        boolean isMatchesEnabled = getArguments().getBoolean(KEY_REGEX_MATCHES);
        //实例化异步任务
        mSearchAsyncTask = new SearchAsyncTask(getActivity(), mCallbacks, mInput, mOpenMode,
                mRootMode, isRegexEnabled, isMatchesEnabled);
        //执行异步任务,将当前查询的路径给到任务中
        mSearchAsyncTask.execute(mPath);
    }

    @Override
    public void onDetach() {
        super.onDetach();

        // to avoid activity instance leak while changing activity configurations
        mCallbacks = null;
    }

}

定义了Activity需要实现的接口,同时启动了搜索的异步任务最后在销毁的方法中释放Activty的CallBack,我们先关注异步任务,然后在去看Activty里面怎么处理接口实现相关内容的

搜索的异步任务

异步任务SearchAsyncTask做了什么?

public class SearchAsyncTask extends AsyncTask<String, HybridFileParcelable, Void> {

    private static final String TAG = "SearchAsyncTask";

    private WeakReference<Activity> activity;
    private SearchWorkerFragment.HelperCallbacks mCallbacks;
    private String mInput;
    private OpenMode mOpenMode;
    private boolean mRootMode, isRegexEnabled, isMatchesEnabled;

    public SearchAsyncTask(Activity a, SearchWorkerFragment.HelperCallbacks l,
                           String input, OpenMode openMode, boolean root, boolean regex,
                           boolean matches) {
        activity = new WeakReference<>(a);
        mCallbacks = l;
        mInput = input;
        mOpenMode = openMode;
        mRootMode = root;
        isRegexEnabled = regex;
        isMatchesEnabled = matches;
    }

    @Override
    protected void onPreExecute() {
         //请注意,我们需要检查每个方法中的回调是否为空,
         //以便在调用Activities和Fragments的onDestroy()方法后调用回调
        if (mCallbacks != null) {
            mCallbacks.onPreExecute(mInput);
        }
    }

    //获取当前的路径
        String path = params[0];
        //实例化混合文件管理类,将路径,以及当前文件模式传递过去
        HybridFile file = new HybridFile(mOpenMode, path);
        file.generateMode(activity.get());
        if (file.isSmb()) return null;

        // level 1
        // if regex or not
        //如果没有正则表达式的搜索
        if (!isRegexEnabled) {
        进入搜索
            search(file, mInput);
        } else {
            // 编译输入中的正则表达式
            Pattern pattern = Pattern.compile(bashRegexToJava(mInput));
            // level 2
            if (!isMatchesEnabled) searchRegExFind(file, pattern);
            else searchRegExMatch(file, pattern);
        }
        return null;
}
    @Override
    public void onPostExecute(Void c) {
        if (mCallbacks != null) {
            mCallbacks.onPostExecute(mInput);
        }
    }

    @Override
    protected void onCancelled() {
        if (mCallbacks != null) mCallbacks.onCancelled();
    }

    @Override
    public void onProgressUpdate(HybridFileParcelable... val) {
        if (!isCancelled() && mCallbacks != null) {
        //搜索到的文件
            mCallbacks.onProgressUpdate(val[0], mInput);
        }
    }

 /**
     * 递归搜索文件名。
     *
     * @param directory the current path
     */
    private void search(HybridFile directory, final SearchFilter filter) {
    //先判断是不是文件夹
        if (directory.isDirectory(activity.get())) {// do you have permission to read this directory?
        //然后遍历文件夹下面的文件
            directory.forEachChildrenFile(activity.get(), mRootMode, new OnFileFound() {
                @Override
                public void onFileFound(HybridFileParcelable file) {
            //没有取消就搜索
                    if (!isCancelled()) {
                        if (filter.searchFilter(file.getName())) {
            //找到文件
                            publishProgress(file);
                        }
            //递归调用
                        if (file.isDirectory() && !isCancelled()) {
                            search(file, filter);
                        }
                    }
                }
            });
        } else {
            Log.d(TAG, "Cannot search " + directory.getPath() + ": Permission Denied");
        }
    }

    private void search(HybridFile file, final String query) {
        search(file, fileName -> fileName.toLowerCase().contains(query.toLowerCase()));
    }
//...

通过上面的异步任务我们需要关注search方法。
其中关于HybridFileParcelable这个类是个实体类,为HybridFile类服务,这个类处理了多个文件路径下数据的问题(包含FTP,多种云盘处理),onFileFound接口来接收文件是否查询到。然后我们看看具体的Search做了什么

HybridFile类中

public void forEachChildrenFile(Context context, boolean isRoot, OnFileFound onFileFound) {
        switch (mode) {
            case SFTP:             
                break;
            case SMB:
                break;
            case OTG:

                break;
            case DROPBOX:
            case BOX:
            case GDRIVE:
            case ONEDRIVE:
               //本地路径处理
            default:
                RootHelper.getFiles(path, isRoot, true, null, onFileFound);

        }
    }

RootHelper是什么?这个类主要处理了关于Root文件权限的问题,其中用到了比较出名的一个库 superUser

 public static void getFiles(String path, boolean root, boolean showHidden,
                                GetModeCallBack getModeCallBack, OnFileFound fileCallback) {
        OpenMode mode = OpenMode.FILE;
        //设置bean,封装,打包好
        ArrayList<HybridFileParcelable> files = new ArrayList<>();

        //如果是这个目录【/】那就只有root权限才可以进入

        if (root && !path.startsWith("/storage") && !path.startsWith("/sdcard")) {
            try {
                // we're rooted and we're trying to load file with superuser
                // we're at the root directories, superuser is required!
                ArrayList<String> ls;
                String cpath = getCommandLineString(path);
                //ls = Shell.SU.run("ls -l " + cpath);
                ls = runShellCommand("ls -l " + (showHidden ? "-a " : "") + "\"" + cpath + "\"");
                if (ls != null) {
                    for (int i = 0; i < ls.size(); i++) {
                        String file = ls.get(i);
                        if (!file.contains("Permission denied")) {
                            HybridFileParcelable array = FileUtils.parseName(file);
                            if (array != null) {
                                array.setMode(OpenMode.ROOT);
                                array.setName(array.getPath());
                                array.setPath(path + "/" + array.getPath());
                                if (array.getLink().trim().length() > 0) {
                                    boolean isdirectory = isDirectory(array.getLink(), root, 0);
                                    array.setDirectory(isdirectory);
                                } else array.setDirectory(isDirectory(array));
                                files.add(array);
                                fileCallback.onFileFound(array);
                            }
                        }
                    }
                    mode = OpenMode.ROOT;
                }

                if (getModeCallBack != null) getModeCallBack.getMode(mode);
            } catch (ShellNotRunningException e) {
                e.printStackTrace();
            }
        }
        //当且仅当此抽象路径名指定的文件存在且 可被应用程序读取时,返回 true;否则返回 false 
        //是否是文件夹
        if (FileUtils.canListFiles(new File(path))) {
            // 利用java提供的文件类加载类
            getFilesList(path, showHidden, fileCallback);
            mode = OpenMode.FILE;
        } else {

            mode = OpenMode.FILE;
        }

        if (getModeCallBack != null) getModeCallBack.getMode(mode);
    }




public static ArrayList<String> runShellCommand(String cmd) throws ShellNotRunningException {
        if (MainActivity.shellInteractive == null || !MainActivity.shellInteractive.isRunning())
            throw new ShellNotRunningException();
        final ArrayList<String> result = new ArrayList<>();

        // 在后台线程上处理回调
        MainActivity.shellInteractive.addCommand(cmd, 0, (commandCode, exitCode, output) -> {
            for (String line : output) {
                result.add(line);
            }
        });
        MainActivity.shellInteractive.waitForIdle();
        return result;
    }

public static ArrayList<HybridFileParcelable> getFilesList(String path, boolean showHidden, OnFileFound listener) {
        File f = new File(path);
        ArrayList<HybridFileParcelable> files = new ArrayList<>();
        try {
            if (f.exists() && f.isDirectory()) {
                for (File x : f.listFiles()) {
                    long size = 0;
                    if (!x.isDirectory()) size = x.length();
                    HybridFileParcelable baseFile = new HybridFileParcelable(x.getPath(), parseFilePermission(x),
                            x.lastModified(), size, x.isDirectory());
                    baseFile.setName(x.getName());
                    baseFile.setMode(OpenMode.FILE);
                    if (showHidden) {
                        files.add(baseFile);
                        listener.onFileFound(baseFile);
                    } else {
                        if (!x.isHidden()) {
                            files.add(baseFile);
                            listener.onFileFound(baseFile);
                        }
                    }
                }
            }
        } catch (Exception e) {
        }
        return files;
    }

搜索文件也是用的Java提供的listFiles(),通过给定的路径然后查找路径下面的全部文件,如果目录下面的文件名包含搜索的字符那就异步通知。
如果不是那就递归调用继续扫描。

if (!isCancelled()) {
if (filter.searchFilter(file.getName())) {
//找到文件
publishProgress(file);
}
//递归调用
if (file.isDirectory() && !isCancelled()) {
search(file, filter);
}
}

到这里我们就已经知道了整个异步搜索的过程了。接着来看Activity与Fragment怎么处理数据显示的问题。也就是异步任务执行的过程中,怎么告诉对应的界面去更新。

视图处理

上面说到在用户点击搜索的按钮时候,会加入一个新的Fragment(SearchWorkerFragment ),同时在这个fragment定义了几个接口,然后去Activity里面实现。

@Override
    public void onPreExecute(String query) {
        mainFragment.mSwipeRefreshLayout.setRefreshing(true);
        mainFragment.onSearchPreExecute(query);
    }

    @Override
    public void onPostExecute(String query) {
        mainFragment.onSearchCompleted(query);
        mainFragment.mSwipeRefreshLayout.setRefreshing(false);
    }

    @Override
    public void onProgressUpdate(HybridFileParcelable val , String query) {
        mainFragment.addSearchResult(val,query);
    }

    @Override
    public void onCancelled() {
        mainFragment.reloadListElements(false, false, !mainFragment.IS_LIST);
        mainFragment.mSwipeRefreshLayout.setRefreshing(false);
    }

代码里面看,所有的工作都交给了MainFragment去处理。

搜索JPG图片

搜索结果

//用于预搜索准备工作
public void onSearchPreExecute(String query) {
        getMainActivity().getAppbar().getBottomBar().setPathText("");
        getMainActivity().getAppbar().getBottomBar().setFullPathText(getString(R.string.searching, query));
    }


    // 
    public void addSearchResult(HybridFileParcelable a, String query) {
        if (listView != null) {

            // initially clearing the array for new result set
            if (!results) {
                LIST_ELEMENTS.clear();
                file_count = 0;
                folder_count = 0;
            }

            // adding new value to LIST_ELEMENTS
            //将查找到的文件一点一点的加入
            LayoutElementParcelable layoutElementAdded = addTo(a);
            if (!results) {
            //重新加载文件列表
                reloadListElements(false, false, !IS_LIST);
                getMainActivity().getAppbar().getBottomBar().setPathText("");
                getMainActivity().getAppbar().getBottomBar().setFullPathText(getString(R.string.searching, query));
                results = true;
            } else {
            //将数据加入集合中,然后再notifyItemInserted
                adapter.addItem(layoutElementAdded);
            }
            stopAnimation();
        }
    }
    //搜索完成后,文件排序
    public void onSearchCompleted(final String query) {
        if (!results) {
            // no results were found
            LIST_ELEMENTS.clear();
        }
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                Collections.sort(LIST_ELEMENTS, new FileListSorter(dsort, sortby, asc));
                return null;
            }

            @Override
            public void onPostExecute(Void c) {
                //重新加载文件列表
                reloadListElements(true, true, !IS_LIST);
                getMainActivity().getAppbar().getBottomBar().setPathText("");
                getMainActivity().getAppbar().getBottomBar().setFullPathText(getString(R.string.searchresults, query));
            }
        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

我们看到在MainActivity里面的onPostExecute()方法中去执行了onSearchCompleted(),因为查找到的文件一点一点的加入了集合中,所以作者又启动了一个异步任务,去排序,重新Load列表。

搜索的大致流程就是这样的了,Amaze文件夹管理器里面的代码很复杂,看起来晕乎乎的。不过看懂之后,你就会发现里面有些技巧,代码写作方式很值得我去学习。

  • 1
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值