我们来看一下文件夹管理器Amaze是怎么实现Search操作的。一般来说,文件的操作与处理我们都会大量的使用Java提供的File类。
通过各种File操作,来达到我们的目的。
用户点击搜索框并且输入字符这时候做了什么?
//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) {}
});
}
到了这里我们看到了怎么显示出搜索栏,接着看看如何处理搜索数据。
在MainActivity
的onCreate()
方法中有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;
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 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 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 runShellCommand(String cmd) throws ShellNotRunningException {
if (MainActivity.shellInteractive == null || !MainActivity.shellInteractive.isRunning())
throw new ShellNotRunningException();
final ArrayList 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 getFilesList(String path, boolean showHidden, OnFileFound listener) {
File f = new File(path);
ArrayList 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去处理。
//用于预搜索准备工作
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() {
@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文件夹管理器里面的代码很复杂,看起来晕乎乎的。不过看懂之后,你就会发现里面有些技巧,代码写作方式很值得我去学习。