预期效果:通过点击文件夹或文件,选择文件或文件夹,并返回选择的路径

除了作为笔记,还有一点就是学习过程中看到好的教程太少了,基本上都是零散的代码片段,或者版本陈旧,无法参考,因此发了最完整的带各种注释的文件供初学者实践参考。

工具:android studio Java环境   可调式的android设备

效果图:

下面附上真正的完整代码:

下面几个文件是要自己写的,其他都为系统自动生成。

MainActivity.java   ,用于控制主界面。

package com.example.storage_path_select;import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.storage.StorageManager;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.activity.result.contract.ActivityResultContracts.GetContent;import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;public class MainActivity extends AppCompatActivity{public static final int FILE_RESULT_CODE = 1;public static final int FLAG_SUCCESS = 1;//创建成功public static final int FLAG_EXISTS = 2;//已存在public static final int FLAG_FAILED = 3;//创建失败public static String TAG="wrong message";public String CacheDir;//缓存文件夹地址,一定可以访问private Button btn_open;private TextView changePath;private String rootPath;myResultContracts M;ActivityResultContracts.StartActivityForResult mRC=new ActivityResultContracts.StartActivityForResult();ActivityResultLauncher<Intent>mGetContent =registerForActivityResult(new ActivityResultContract<Intent, Intent>() {@NonNull@Overridepublic Intent createIntent(@NonNull Context context, Intent intent) {return intent;}@Overridepublic Intent parseResult(int i, @Nullable Intent intent) {return intent;}},new ActivityResultCallback<Intent>() {public void onActivityResult(Intent data) {Bundle bundle = null;if (data != null && (bundle = data.getExtras()) != null) {String path = bundle.getString("file", "");if (!path.isEmpty()) {changePath.setText("选择路径为 : " + path);}}}});@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);askForPower();//动态申请内存权限initView();initListener();CacheDir=this.getExternalCacheDir().getAbsolutePath();//缓存文件夹地址,一定可以访问for(int i=1;i<=20;i++){int result=createFile(CacheDir+"/test"+String.valueOf(i)+".txt");}//存放20个共测试用的文件//Toast.makeText(this, String.valueOf(result), Toast.LENGTH_LONG).show();//创建失败,不能直接在"/storage/9F4F-28D0/android”目录下创建文件}private void initView() {btn_open = (Button) findViewById(R.id.btn_open);changePath = (TextView) findViewById(R.id.changePath);}//初始化,绑定控件private void initListener() {btn_open.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {openBrowser();}});//打开系统存储findViewById(R.id.btn_open1).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {openBrowser1();}});//打开SD卡存储}private void openBrowser() {//打开系统存储rootPath = System.getenv("SECONDARY_STORAGE");//获得环境变量中特定的值(路径)if (rootPath == null) {rootPath = Environment.getExternalStorageDirectory().toString();}if ((rootPath.equals(Environment.getExternalStorageDirectory().toString()))) {final String filePath = rootPath + "/Android";Intent intent = new Intent(MainActivity.this, FileBrowserActivity.class);//根目录//intent.putExtra("rootPath", rootPath);//进入默认文件夹(根目录)intent.putExtra("rootPath","/storage/9F4F-28D0/android");//优先进去指定文件夹intent.putExtra("path", CacheDir);//storage文件夹还是没有访问权限,直接进入会程序崩溃mGetContent.launch(intent);//startActivityForResult(intent, FILE_RESULT_CODE);//已弃用//如果想在Activity中得到新打开Activity 关闭后返回的数据,//需要使用系统提供的startActivityForResult(Intent intent, int requestCode)方法打开新的Activity,//新的Activity 关闭后会向前面的Activity传回数据,为了得到传回的数据,//必须在前面的Activity中重写onActivityResult(int requestCode, int resultCode, Intent data)方法。/*DeprecatedThis method has been deprecated in favor of using the Activity Result API which brings increasedtype safety via an ActivityResultContract and the prebuilt contracts for common intents available inandroidx.activity.result.contract.ActivityResultContracts, provides hooks for testing,and allow receiving results in separate, testable classes independent from your activity.Use registerForActivityResult(ActivityResultContract, ActivityResultCallback)passing in a StartActivityForResult object for the ActivityResultContract.启动另一个 activity(无论是您应用中的 activity 还是其他应用中的 activity)不一定是单向操作。您也可以启动另一个 activity 并接收返回的结果。例如,您的应用可启动相机应用并接收拍摄的照片作为结果。或者,您可以启动“通讯录”应用以便用户选择联系人,并且您将接收联系人详细信息作为结果。虽然所有 API 级别的 Activity 类均提供底层 startActivityForResult() 和 onActivityResult() API,但我们强烈建议您使用 AndroidX Activity 和 Fragment 中引入的 Activity Result API。Activity Result API 提供了用于注册结果、启动结果以及在系统分派结果后对其进行处理的组件。*///registerForActivityResult(myResultContracts);/*针对 activity 结果注册回调在启动 activity 以获取结果时,可能会出现您的进程和 activity 因内存不足而被销毁的情况;如果是使用相机等内存密集型操作,几乎可以确定会出现这种情况。因此,Activity Result API 会将结果回调从您之前启动另一个 activity 的代码位置分离开来。由于在重新创建进程和 activity 时需要使用结果回调,因此每次创建 activity 时都必须无条件注册回调,即使启动另一个 activity 的逻辑仅基于用户输入内容或其他业务逻辑也是如此。位于 ComponentActivity 或 Fragment 中时,Activity Result API 会提供 registerForActivityResult() API,用于注册结果回调。registerForActivityResult() 接受 ActivityResultContract 和 ActivityResultCallback 作为参数,并返回 ActivityResultLauncher,供您用来启动另一个 activity。ActivityResultContract 定义生成结果所需的输入类型以及结果的输出类型。这些 API 可为拍照和请求权限等基本 intent 操作提供默认协定。您还可以创建自己的自定义协定。ActivityResultCallback 是单一方法接口,带有 onActivityResult() 方法,可接受 ActivityResultContract 中定义的输出类型的对象:*//*启动 activity 以获取其结果虽然 registerForActivityResult() 会注册您的回调,但它不会启动另一个 activity 并发出结果请求。这些操作由返回的 ActivityResultLauncher 实例负责。如果存在输入内容,启动器会接受与 ActivityResultContract 的类型匹配的输入内容。调用 launch() 会启动生成结果的过程。当用户完成后续 activity 并返回时,系统将执行 ActivityResultCallback 中的 onActivityResult(),如以下示例所示:*/}}private void openBrowser1() {//打开SD卡存储rootPath = getSdcardPath();if (rootPath == null || rootPath.isEmpty()) {rootPath = Environment.getExternalStorageDirectory().toString();}//Toast.makeText(this, "rootPath:"+rootPath, Toast.LENGTH_SHORT).show();//  /storage/9F4F-28D0Intent intent = new Intent(MainActivity.this, FileBrowserActivity.class);intent.putExtra("rootPath", rootPath);intent.putExtra("path", rootPath);//putExtra方法用于向下一个activity传递数据mGetContent.launch(intent);//startActivityForResult(intent, FILE_RESULT_CODE);}public String getSdcardPath() {String sdcardPath = "";String[] pathArr = null;StorageManager storageManager = (StorageManager) getSystemService(STORAGE_SERVICE);try {Method getVolumePaths = storageManager.getClass().getMethod("getVolumePaths");pathArr = (String[]) getVolumePaths.invoke(storageManager);} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}if (pathArr != null && pathArr.length >= 3) {sdcardPath = pathArr[1];}return sdcardPath;}/*@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {if (FILE_RESULT_CODE == requestCode) {Bundle bundle = null;if (data != null && (bundle = data.getExtras()) != null) {String path = bundle.getString("file","");if(!path.isEmpty()){changePath.setText("选择路径为 : " + path);}}}}当新Activity关闭后,新Activity返回的数据通过Intent进行传递,android平台会调用前面Activity 的onActivityResult()方法,把存放了返回数据的Intent作为第三个输入参数传入,在onActivityResult()方法中使用第三个输入参数可以取出新Activity返回的数据。*/public void askForPower(){//用于android10和android11手动申请权限if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Environment.isExternalStorageManager()){Toast.makeText(this, "已获得访问所有文件权限", Toast.LENGTH_SHORT).show();} else {AlertDialog.Builder builder =new AlertDialog.Builder(this);builder.setMessage("本程序需要您同意允许访问所有文件权限");builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);startActivity(intent);}});builder.show();}}public static int createFile(String filePath) {File file = new File(filePath);if (file.exists()) {Log.e(TAG, "The file [ " + filePath + " ] has already exists");return FLAG_EXISTS;}if (filePath.endsWith(File.separator)) {// 以 路径分隔符 结束,说明是文件夹Log.e(TAG, "The file [ " + filePath + " ] can not be a directory");return FLAG_FAILED;}//判断父目录是否存在if (!file.getParentFile().exists()) {//父目录不存在 创建父目录Log.d(TAG, "creating parent directory...");if (!file.getParentFile().mkdirs()) {Log.e(TAG, "created parent directory failed.");return FLAG_FAILED;}}//创建目标文件try {if (file.createNewFile()) {//创建文件成功Log.i(TAG, "create file [ " + filePath + " ] success");return FLAG_SUCCESS;}} catch (IOException e) {e.printStackTrace();Log.e(TAG, "create file [ " + filePath + " ] failed");return FLAG_FAILED;}return FLAG_FAILED;}
}

FileBroswerActivity.java   用于控制第二个activity,作为筛选文件的页面控制

package com.example.storage_path_select;import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;import java.io.File;public class FileBrowserActivity extends Activity implements View.OnClickListener, MyAdapter.FileSelectListener {private TextView curPathTextView;private String rootPath = "";private MyAdapter listAdapter;//初始化进入的目录,默认目录private String filePath = "";protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_file_browser_acitivity);initView();//根目录rootPath = getIntent().getStringExtra("rootPath");//优先进入指定文件夹filePath = getIntent().getStringExtra("path");//getIntent()用于获得上一个activity传来的信息curPathTextView.setText(filePath);filePath = filePath.isEmpty() ? rootPath : filePath;View layoutFileSelectList = findViewById(R.id.layoutFileSelectList);//Toast.makeText(this, filePath, Toast.LENGTH_SHORT).show();listAdapter = new MyAdapter(layoutFileSelectList, rootPath, filePath,this.getApplicationContext());listAdapter.setOnFileSelectListener(this);//this为什么数据类型和自定义的接口是相同类型?findViewById(R.id.btnSure).setOnClickListener(this);findViewById(R.id.btnCancel).setOnClickListener(this);}@Overridepublic void finish() {Intent intent = new Intent();intent.putExtra("file", filePath);setResult(RESULT_OK, intent);super.finish();}//重写finish函数,用putExtra方法向前一个activity传递filePathprivate void initView() {curPathTextView = (TextView) findViewById(R.id.curPath);}@Overridepublic void onFileSelect(File selectedFile) {filePath = selectedFile.getPath();}@Overridepublic void onDirSelect(File selectedDir) {filePath = selectedDir.getPath();}@Overridepublic void onClick(View v) {switch (v.getId()){case R.id.btnSure:finish();break;case R.id.btnCancel:filePath ="";finish();break;default:break;}}//确定或取消}

MyAdaper.java   重写适配器,用于选择文件的ListView中填入文件信息

package com.example.storage_path_select;//import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;import androidx.annotation.Nullable;import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;//简单理解就是adapter是view和数据的桥梁。在一个ListView或者GridView中,你不可能手动给每一个格子都新建一个view,
//所以这时候就需要Adapter的帮忙,它会帮你自动绘制view并且填充数据。/*
学会BaseAdapter其实只需要掌握四个方法:
getCount, getItem, getItemId, getViewgetCount : 要绑定的条目的数目,比如格子的数量
getItem : 根据一个索引(位置)获得该位置的对象
getItemId : 获取条目的id
getView : 获取该条目要显示的界面
可以简单的理解为,adapter先从getCount里确定数量,然后循环执行getView方法将条目一个一个绘制出来,所以必须重写的是getCount和getView方法。
而getItem和getItemId是调用某些函数才会触发的方法,如果不需要使用可以暂时不修改。*/public class MyAdapter extends BaseAdapter implements View.OnClickListener, AdapterView.OnItemClickListener {private String rootPath;private LayoutInflater mInflater;//LayoutInflater是一个用于将xml布局文件加载为View或者ViewGroup对象的工具,我们可以称之为布局加载器。private Bitmap mIcon3;private Bitmap mIcon4;private List<File> fileList;//<> 是 java 泛型的表示形式,泛型是JDK1.5引入的新特性,泛型的其本质是实例化类型//在 List 中添加数据是使用 add() 方法,而获取 List 内容则是使用 get() 方法。//此处为存放所有待展示的文件列表private View header;private View layoutReturnRoot;private View layoutReturnPre;private TextView curPathTextView;private String suffix = "";private String currentDirPath;private FileSelectListener listener;//设置监听器public MyAdapter(View fileSelectListView, String rootPath, String defaultPath,Context Appcontext) {//没有返回类型this.rootPath = rootPath;Context context = fileSelectListView.getContext();mInflater = LayoutInflater.from(context);mIcon3 = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon_folder);mIcon4 = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon_file);curPathTextView = (TextView) fileSelectListView.findViewById(R.id.curPath);header = fileSelectListView.findViewById(R.id.layoutFileListHeader);//header为返回键的layoutlayoutReturnRoot = fileSelectListView.findViewById(R.id.layoutReturnRoot);layoutReturnPre = fileSelectListView.findViewById(R.id.layoutReturnPre);layoutReturnRoot.setOnClickListener(this);layoutReturnPre.setOnClickListener(this);if (defaultPath != null && !defaultPath.isEmpty()) {getFileDir(defaultPath);//Toast.makeText(Appcontext, "运行到1处", Toast.LENGTH_SHORT).show();//运行至此} else {getFileDir(rootPath);//Toast.makeText(Appcontext, "运行到2处", Toast.LENGTH_SHORT).show();}//错误代码处ListView listView = (ListView) fileSelectListView.findViewById(R.id.list);listView.setAdapter(this);//BuglistView.setOnItemClickListener(this);/**/}private class ViewHolder {TextView text;ImageView icon;}//存放用于显示的viewpublic interface FileSelectListener {void onFileSelect(File selectedFile);void onDirSelect(File selectedDir);}public void setOnFileSelectListener(FileSelectListener listener) {this.listener = listener;}    //listener用于监听某个动作并做出响应。这个函数用来干啥的?将外部的监听和ListView的监听统一//this的三个用法://调用本类中的成员变量//调用本类中的其他方法//代表当前对象/*** 获取所选文件路径下的所有文件,并且更新到listview中*/private void getFileDir(String filePath) {File file = new File(filePath);File[] files = file.listFiles(/*new FileFilter() {//File类中的listFiles()得到的是一个 File 类型的数组,返回的是该目录中的文件和目录。@Overridepublic boolean accept(File pathname) {if (pathname.isFile()){                         //&&!suffix.isEmpty()return pathname.getName().endsWith(suffix);//endsWith() 方法用于测试字符串是否以指定的后缀结束。}//Tests whether or not the specified abstract pathname should be included in a pathname list.//Params:pathname – The abstract pathname to be tested//Returns:true if and only if pathname should be included//suffix:后缀//isfile():Tests whether the file denoted by this abstract pathname is a normal file. A file is normal if it is not a directory and,//in addition, satisfies other system-dependent criteria. Any non-directory file created by a Java application is guaranteed to be a normal file.return true;}}*/);//Toast.makeText(Appcontext, "一共有"+String.valueOf(files.length)+"文件", Toast.LENGTH_SHORT).show();if(files!=null)fileList = Arrays.asList(files);//该方法是将数组转化成List集合的方法。else fileList=new ArrayList<>();Log.e("filelist的数量:",String.valueOf(fileList.size()));//按名称排序Collections.sort(fileList, new Comparator<File>() {@Overridepublic int compare(File o1, File o2) {if (o1.isFile() && o2.isDirectory())       //java中的isDirectory ()是检查一个对象是否是文件夹。return 1;if (o1.isDirectory() && o2.isFile())return -1;return o1.getName().compareTo(o2.getName());}});if (header != null) {header.setVisibility(filePath.equals(rootPath) ? View.GONE : View.VISIBLE);//若等于根目录,则隐藏返回键}notifyDataSetChanged();//Notifies the attached observers that the underlying data is no longer valid or available.// Once invoked this adapter is no longer valid and should not report further data set changes.if (curPathTextView != null) {curPathTextView.setText(filePath);//设置当前目录}currentDirPath = filePath;if (listener!=null){listener.onDirSelect(file);}//为啥写在这里?/**/}public int getCount() {return fileList.size();}//getCount的返回值为我们的数据源的长度,要显示几个条目,我们就传入多长的数据源.这里的长度为filelist的大小public Object getItem(int position) {return fileList.get(position);}//获取数据集中与索引对应的数据项public long getItemId(int position) {return position;}//获取指定行对应的ID/** @param position 表示当前索引* @param convertView 缓存区   就是这个convertView其实就是最关键的部分*                         原理上讲 当ListView滑动的过程中 会有item被滑出屏幕 而不再被使用 这时候Android会*                        回收这个条目的view 这个view也就是这里的convertView*                    也就是为了不浪费内存重新调用第一个地址      就是被划上去的那个View*                    当item1被移除屏幕的时候 我们会重新new一个View给新显示的item_new*                    而如果使用了这个convertView 我们其实可以复用它 这样就省去了new View的大量开销* @param parent 每个ItemView里面的容器  返回的View直接添加到容器中来* @return View 就是每个ItemView要显示的内容*//**/public View getView(int position, View convertView, ViewGroup parent) {//设置每一行Item的显示内容。//viewgroup为盛放控件的容器,如linearLayout,RelativeLayoutViewHolder holder;/*使用ViewHolder优化在getView方法中,Adapter先从xml中用inflate方法创建view对象,然后在这个view找到每一个控件.这里的findViewById操作是一个树查找过程,也是一个耗时的操作,所以这里也需要优化,就是使用viewHolder,把每一个控件都放在Holder中,当第一次创建convertView对象时,把这些控件找出来。然后用convertView的setTag将viewHolder设置到Tag中,以便系统第二次绘制ListView时从Tag中取出。当第二次重用convertView时,只需从convertView中getTag取出来就可以。*/if (convertView == null) {//第一次创建convertView//convertView是旧视图,就是绘制好了的视图;parent是父级视图,也就是ListView之类的。//上面的convertView是旧视图是什么意思呢?就是listview如果超出了屏幕,滑动的时候会隐藏掉一部分,//这时候就将隐藏掉的部分保存到convertView中。那么如果是我们之前的写法,每次返回的时候就没有使用convertView,重新创建了一个View,// 这样子浪费了系统资源。那要怎么利用convertView优化呢?使用ViewHolder优化//先判断convertView是否为空,是的话就创建ViewHolder,不是的话就取出ViewHolder,这样就可以实现复用convertView了convertView = mInflater.inflate(R.layout.file_item, null);/*加载布局的方法:public View inflate (int resource, ViewGroup root, boolean attachToRoot)该方法的三个参数依次为:①要加载的布局对应的资源id②为该布局的外部再嵌套一层父布局,如果不需要的话,写null就可以了!③是否为加载的布局文件的最外层套一层root布局,不设置该参数的话,如果root不为null的话,则默认为true 如果root为null的话,attachToRoot就没有作用了!root不为null,attachToRoot为true的话,会在加载的布局文件最外层嵌套一层root布局;为false的话,则root失去作用! 简单理解就是:是否为加载的布局添加一个root的外层容器*/holder = new ViewHolder();holder.text = (TextView) convertView.findViewById(R.id.text);if(holder.text==null)Log.e("holder.text","空指针");//报错:虚方法用在空指针上holder.icon = (ImageView) convertView.findViewById(R.id.icon);convertView.setTag(holder);} else {holder = (ViewHolder) convertView.getTag();//View中的setTag(Onbect)表示给View添加一个格外的数据,以后可以用getTag()将这个数据取出来。}File file = fileList.get(position);//getView方法中的三个参数,position是指现在是第几个条目;convertView是旧视图,就是绘制好了的视图;if(holder.text!=null)holder.text.setText(file.getName());//报错:虚方法用在空指针上else Log.e("holder.text","空指针");//else holder.text.setText("未命名");if (file.isDirectory()) {holder.icon.setImageBitmap(mIcon3);//设置为文件夹图标} else {holder.icon.setImageBitmap(mIcon4);//设置为文件图标}return convertView;}/*LayoutInflater是用来加载布局的,其中parent是父级视图,也就是ListView之类的。用inflate方法绘制好后的view最后return返回给getView方法就可以了。*/@Overridepublic void onItemClick(AdapterView<?> parent, View view, int position, long id) {//parent:parent相当于listview适配器的一个指针,可以通过它来获得listview里装着的一切东西,简单说就是所使用的list容器,//例如ListView、GridView。通过强制类型转换可以将parent转换为对应的list容器。//然后通过转换得到的list对象调用getAdapter()方法获得适配对象,通过适配对象就可以获得所展示的每一项的对象model。//view是你点的b这个view的句柄,就是你可以用这个view,来获得b里的控件的id后操作控件。//就是可以使用 view.findViewById()方法来获取所点击item中的控件。//position是b在适配器里的位置(生成listview时,适配器一个一个的做item,然后把他们按顺序排好队,在放到listview里,意思就是这个b是第position号做好的)//id是所点击项在listview里的第几行的位置,大部分时候position和id的值是一样的,如果需要的话,你可以自己加个log把position和id都弄出来在logcat里瞅瞅File file = fileList.get(position);Log.e("onItemClick:","点击事件"+String.valueOf(position)+"触发");if (file.isDirectory()) {getFileDir(file.getPath());//判断是否点击了文件夹或文件} else {if (listener!=null){listener.onFileSelect(file);Log.e("onTtemClick:","点击文件事件生效");Toast.makeText(curPathTextView.getContext(), "选择文件", Toast.LENGTH_SHORT).show();if (curPathTextView != null) {curPathTextView.setText(file.getAbsolutePath());//设置当前目录}}else Log.e("onItemClick","Listener为null");}}@Overridepublic void onClick(View v) {if (v.getId() == R.id.layoutReturnRoot) {getFileDir(rootPath);//返回根目录} else if (v.getId() == R.id.layoutReturnPre) {getFileDir(new File(currentDirPath).getParent());//返回上一级目录}}//返回或取消
}

myResultContracts.java  没有用,可以不要的文件,之前写Contracts写废了,发现在主函数中直接构造更好一些,就不用这个文件了。

res文件:

drawable中,四个jpg文件是可以自己随便找的,就是用于显示些图标的文件。

shape_divider_line.xml  用于设置分割线的格式。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"><solid android:color="@color/black" /><size android:height="1px" />
</shape>

layout文件

activity_file_browser_activity.xml 用于展示筛选文件页面

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/layoutProgramManagerMainView"android:layout_width="match_parent"android:layout_height="match_parent"android:focusable="true"android:focusableInTouchMode="true"android:orientation="vertical"><includelayout="@layout/layout_file_select_list"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="50dp"android:layout_gravity="bottom"android:layout_weight="0"android:orientation="vertical"android:showDividers="beginning"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:orientation="horizontal"><Buttonandroid:id="@+id/btnCancel"android:layout_width="0dp"android:layout_height="40dp"android:layout_weight="1"android:text="@string/cancel"android:textSize="16sp" /><Buttonandroid:id="@+id/btnSure"android:layout_width="0dp"android:layout_height="40dp"android:layout_weight="1"android:text="@string/sure"android:textSize="16sp" /></LinearLayout></LinearLayout></LinearLayout>

activity_main.xml  用于展示主页面

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/activity_main"android:layout_width="match_parent"android:layout_height="match_parent"android:focusable="true"android:focusableInTouchMode="true"android:orientation="vertical"android:background="@color/white"><TextViewandroid:id="@+id/changePath"android:layout_width="match_parent"android:layout_height="470dp"android:text="@string/path"android:textColor="@color/black"/><Buttonandroid:id="@+id/btn_open"android:layout_width="match_parent"android:layout_height="72dp"android:layout_marginTop="20dp"android:layout_marginBottom="0dp"android:gravity="center"android:text="@string/open"android:textSize="16sp" /><Buttonandroid:id="@+id/btn_open1"android:layout_width="match_parent"android:layout_height="72dp"android:layout_marginTop="0dp"android:gravity="center"android:text="@string/open1"android:textSize="16sp" /></LinearLayout>

file_item.xml  用于存放listview的子View

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"><ImageViewandroid:id="@+id/icon"android:layout_width="80dp"android:layout_height="80dp"android:layout_marginLeft="20dp" /><TextViewandroid:id="@+id/text"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:layout_marginLeft="120dp"android:textSize="24dp"android:textColor="@color/black"/></LinearLayout>

layout_file_select_list.xml  作为activity_file_broswer_activity.xml的子文件,供其调用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:background="@color/white"android:layout_height="wrap_content"><LinearLayoutandroid:id="@+id/layoutFileSelectList"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginTop="10dp"android:orientation="vertical"android:showDividers="middle|end"><TextViewandroid:id="@+id/curPath"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginBottom="6dp"android:paddingLeft="10dp"android:textSize="16sp"android:textColor="@color/black"/><LinearLayoutandroid:id="@+id/layoutFileListHeader"android:layout_width="match_parent"android:layout_height="wrap_content"android:divider="@drawable/shape_divider_line"android:showDividers="beginning|middle|end"android:orientation="vertical"><LinearLayoutandroid:id="@+id/layoutReturnRoot"android:layout_width="match_parent"android:layout_height="50dp"android:paddingLeft="10dp"android:gravity="center"android:orientation="horizontal"><ImageViewandroid:id="@+id/iv_return_root"android:layout_width="30dp"android:layout_height="30dp"android:background="@drawable/icon_back"></ImageView><TextViewandroid:id="@+id/tv_return_root"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:gravity="center_vertical"android:paddingLeft="10dp"android:text="@string/ReturnRootDir"android:textSize="16sp"android:textColor="@color/black"></TextView></LinearLayout><LinearLayoutandroid:id="@+id/layoutReturnPre"android:layout_width="match_parent"android:layout_height="50dp"android:paddingLeft="10dp"android:gravity="center"android:orientation="horizontal"><ImageViewandroid:id="@+id/iv_return_pre"android:layout_width="30dp"android:layout_height="30dp"android:background="@drawable/icon_back02"></ImageView><TextViewandroid:id="@+id/tv_return_pre"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:gravity="center_vertical"android:paddingLeft="10dp"android:text="@string/ReturnPreDir"android:textSize="16sp"android:textColor="@color/black"></TextView></LinearLayout></LinearLayout><ListViewandroid:id="@+id/list"android:layout_width="match_parent"android:layout_height="match_parent"></ListView></LinearLayout></LinearLayout>

values文件

strings.xml  用于管理相关的字符串常量

<resources><string name="app_name">Storage_path_select</string><string name="cancel">取消</string><string name="sure">确定</string><string name="ReturnRootDir">返回根目录</string><string name="ReturnPreDir">上一级目录</string><string name="open">系统存储</string><string name="open1">sd卡</string><string name="path">路径</string>
</resources>

manifest文件

AndroidManifest.xml   用于管理活动注册,app权限申请的文件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"package="com.example.storage_path_select"><permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /><uses-permission android:name="android.permission.INTERNET"/><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/><uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"tools:ignore="ScopedStorage"/><applicationandroid:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.Storage_path_select"android:debuggable="false"tools:targetApi="31"tools:ignore="HardcodedDebugMode"><activityandroid:name=".MainActivity"android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><activity android:name=".FileBrowserActivity"></activity></application></manifest>

Gradle Scripts 文件

build.gradle(Module:Storage_path_selsect.app)  用于管理编译器的依赖库和版本的文件

plugins {id 'com.android.application'
}android {compileSdk 32defaultConfig {applicationId "com.example.storage_path_select"minSdk 21targetSdk 32versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}buildToolsVersion '33.0.0'
}dependencies {def appcompat_version = "1.5.0"implementation "androidx.appcompat:appcompat:$appcompat_version"implementation "androidx.appcompat:appcompat-resources:$appcompat_version"implementation "androidx.fragment:fragment:1.5.2"implementation 'com.google.android.material:material:1.4.0'implementation 'androidx.constraintlayout:constraintlayout:2.0.4'implementation 'androidx.activity:activity:1.5.1'testImplementation 'junit:junit:4.13.2'androidTestImplementation 'androidx.test.ext:junit:1.1.3'androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

以上就是需要编写的文件。

总结

花了几天时间,做这个小程序,初步学习并体验了一下android开发。没想到看似这么基础的功能,也需要五六百行代码来实现。学习过程就是把网上前人的代码片段扒下来,然后一点点看懂,纠错,完善,通过看懂别人写的代码来学习。把学习到的东西,后来遇到的一些困难,以及解决的方法,作为笔记整理记录一下。

1.第一次深入了解面向对象语言,了解了面向对象语言的很多基础知识,如基类,派生类,接口,继承,implements的使用条件和规范,this的各种用法,和一些基础的方法,如super()。第一次学习xml文件的标签语言,如何用xml文件组织运行程序的各种资源,以及不同activity之间的通信方法。

2.学习了android软件开发的一些流程,从package下的文件构成,res下文件的作用,gradle的使用,manifest中注册activity,签名打包成apk文件,初步了解了整个android开发的基本流程。

3.了解了一些在android开发debug的一些基本方法。

(1)由于没学Kotlin,只会用Java,因此在网上看教程的时候,常常出现看不懂别人写的kotlin代码的情况。这时候可以用android studio中tools的Java和Kotlin代码互相转化的工具,将看不懂的Kotlin代码转化为Java代码帮助理解。

(2)最开始是用usb连接手机和电脑进行调试的,后来发现了一个更方便,更快捷的调试方法,就是用adb插件进行无线调试,网上很多教程,就是注意一点,华为的鸿蒙系统,设置里没有直接进行扫码与android studio链接的功能,需要手动开启电脑的命令框,进行连接,网上也很多教程。

(3)刚开始不知道有LogCat这样一个android studio自带的日志记录软件,程序崩溃退出老是找不到原因,也不会去看报错,就直接用Toast向屏幕发信息来调试。知道LogCat之后,程序崩溃的Log可以看的到,调试就方便了太多了。

(4)对于一些常见的报错和崩溃,有了些认识。比如空指针报错,在Java开发中算是最常见的了。往往就是初始化对象,没有实例化,在之后的使用中就报错,让程序崩溃了。或者是findObjectById的方法,必须先将Object在layout中加载出来,不然绑定的时候程序会直接崩溃退出。所以要养成好习惯,设置好异常流的检查,用try,catch,finally的语句控制检查运行异常。

(5)比较离谱的bug,实在绕不过可以换种方式写。在这次实践中,就有一处写的和官方一摸一样,编译器还报错的,换了种类似的写法,就完全没有问题了。还有些离谱的问题,就只能靠经验积累了。比如有一次突然就不能显示文字了,反复查看才发现字体颜色没设置,与背景混为一体了。

(6)由于android更新换代非常快,经常会出现一些方法突然用不了了,过时了之类的问题。这时候就需要仔细鉴别和设置各种版本了。利用gradle下的版本管理,非常的方便,可以很容易的设置sdk版本,编译器版本,依赖库资源等各种版本。遇到最新的问题时,可能有用的中文教程非常有限,还是需要到官网上学习最新方法的使用。这一次实践中,由于要切换activity,并在不同的activity之间进行通信,原有的教程中,使用的是setActivityForStart()和OnActivitySet方法。在最新的sdk30版本中,已经标记为废弃该方法进行活动间通信了,要使用最新的ActivityContracts,来规范信息传递,让方法更高效安全。网上很多教程都不合适,于是只能去官网上学习了contracts的写法。还有一个版本带来的问题。早在android 4.0之后,系统就开始对app的存储修改权限做出了限制,无法随意修改读取。在最近的android10.0更新之后,又做了一次修改,使得android系统中app的读取权限更小了。在manifest文件中申请的权限,实际上十分小了,仅限于app安装的目录下的cache文件。若要进行越界内存访问,需要更多的内存申请方法。在了解这些版本问题之前,经常被莫名奇妙的bug困扰,这就是为什么,有时候照抄别人代码,程序也会报错崩溃。

(7)关于uft8编码和另一种中文编码不能被app读取路径的问题,其实是不存在的。后面看Log才发现,程序闪退是因为List为null,和编码没关系。比较新的android版本对于中文的支持都挺好了。

4.csdn帮助了我很多,有很多很不错的大佬写的教程。但是面对有些棘手的bug,中文教程看了几十篇也找不到可行方案时,就很头疼了。这时候最好应该直接去官网上学习,看不懂英文也没关系,反正有翻译。官网上的教程可以说是最好最详尽的了,是个高质量的学习途径。

Activity  |  Android 开发者  |  Android Developers

5.有时候发现一个bug改不出来,可以向大佬们请教,相互交流。在一些QQ群中,或者交流网站,都有很多厉害的大佬,说不定困扰很久的问题,大佬一眼就能帮自己点破,而且能了解一些高效学习的方法。这对于初学者来说帮助挺大的。

6.不足和改进:这个程序只能读取app中cache下的文件资料,因为版本问题,高版本的android限制了app对公共存储的访问。如果要申请对其他内存的访问,应该使用最新的访问api协议。对于面向对象编程方法的巧妙之处理解还很浅显。写的代码很容易就变成面向过程了,就失去了面向对象编程的意义。大佬说最新的androiid提供了一个全局管理activity的方法,就不用intent来切换activity了,这样反而更慢。全局管理应该是个好东西,值得学一手。

除了作为笔记,还有一点就是看到好的教程太少了,基本上都是代码片段,或者不完整,或者版本陈旧,无法参考,因此发了个最完整的带各种注释的文件供初学者实践参考。

2022android自定义文本路径选择器java教程相关推荐

  1. java 限制文本框长度_[Java教程]如何限制textarea文本框的输入字数

    [Java教程]如何限制textarea文本框的输入字数 0 2015-12-24 15:00:10 如何限制textarea文本框的输入字数: 在实际应用中,往往需要限制文本框的输入字数的长度,下面 ...

  2. java如何设置文本框提示_[Java教程]一个友好的文本框内显示提示语 jquery 插件

    [Java教程]一个友好的文本框内显示提示语 jquery 插件 0 2014-08-08 18:01:25 插件实现文本框内默认显示提示语,当文本框获得焦点时提示语消失. 如果没有输入或输入为空则失 ...

  3. java教程:JTextField(文本框)组件使用实例|方法

    java教程:JTextField(文本框)组件使用实例|方法 内容导读: JTextField(文本框)组件 JTextField组件实现一个文本框,用来接受用户输入的单行文本信息,JTextFie ...

  4. Java怎么做置顶_[Java教程]自定义置顶TOP按钮

    [Java教程]自定义置顶TOP按钮 0 2015-12-10 22:00:13 简述一下,分为三个步骤: 1. 添加Html代码2. 调整Css样式3. 添加Jquery代码具体代码如下: #GoT ...

  5. java 文本框只读_[Java教程]javascript脚本设置输入框只读的问题

    [Java教程]javascript脚本设置输入框只读的问题 0 2014-04-24 18:00:04 今天在开发中准备通过javascript设置input框只读属性的时候,用document.g ...

  6. 【mysql 5.7】 mysql5.7安装包以及安装教程及其过程详解 包含自定义安装路径

    目录 事件起因 5.7版本安装包下载 安装过程 事件起因 电脑已经用了三年了,c盘是只有128G买来有118G,因为使用了很久了,c盘几乎都快给占满了,然后怎么清理垃圾或是其他什么的软件都只剩下7-8 ...

  7. JAVA教程-JAVA语言基础框架知识学习点-JAVA精通必看

    JAVA教程中文版在线代码示例 1. JAVA语言基础 1. 1. 导言( 17 ) 1. 9. 变量( 6 ) 1. 2. Java关键词( 1 ) 1. 10. 变量范围( 2 ) 1. 3. J ...

  8. HowToDoInJava Java 教程·翻译完成

    原文:HowToDoInJava 协议:CC BY-NC-SA 4.0 欢迎任何人参与和完善:一个人可以走的很快,但是一群人却可以走的更远. ApacheCN 学习资源 目录 核心 Java 教程 什 ...

  9. JAVA教程(二)之面向对象前基础知识

    Java 基础语法 一个 Java 程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来协同工作.下面简要介绍下类.对象.方法和实例变量的概念. **对象:对象是类的一个实例,有状态和行为. ...

最新文章

  1. 2007年你必须学习的10项.NET技术
  2. vsftpd+pam+mysql实现ftp构建
  3. linux 用户态 spinlock,spinlock作用
  4. wxWidgets:wxColourDatabase类用法
  5. 30.32.33.词云图、3D绘图、矩阵可视化、绘制混淆矩阵
  6. python 赋值重置_Python所有赋值语句快速预览
  7. fprintf函数的用法matlab_极力推荐这个Matlab教程
  8. 【精品计划 附录2】- 算法分析
  9. Android studio设置相机权限,如何强制将“android.permission.CAMERA”权限添加到Codename中的清单中...
  10. C#SQL注入检测——特别是对于旧版.NET代码
  11. 【8.0、9.0c】树形列表 列标题 不对齐的问题及解决方案
  12. 【Kettle】第一篇,Pan 的使用
  13. 计算机开机和关机的音乐,电脑开关机音乐设置
  14. Tkinter模拟发送邮箱验证码并在指定时间后验证码过期
  15. 《数据结构》实验报告(一)顺序表基本操作
  16. Extjs6 学习(一)
  17. 基于51单片机的智能温控风扇设计
  18. 《自拍教程48》Python_adb随机地图移图2小时
  19. python 元组拆包_Python笔记004-元组的拆包和命名元组
  20. vscode 突然无法切换输入法(切换中文输入法)

热门文章

  1. 51单片机入门学习 第七天
  2. Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Gavin大咖免费公益课程Rasa Paper论文解析核心版
  3. vue封装qq表情包和符号表情的发送
  4. Matlab 图像几何变换
  5. 浅谈信息安全的职业发展方向规划(乙方安全公司篇)
  6. 所有科技人员是懂计算机的,指出违反什么规律.PDF
  7. 一年降本 40%:基于云服务的技术成本精细化运营策略
  8. Simple Calculator 1.0(有声计算器)
  9. 支持向量机的理解,目前看到的最通透的
  10. 用python的turtle库绘制风车动画