2022android自定义文本路径选择器java教程

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

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

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

效果图:

2022android自定义文本路径选择器java教程_第1张图片

 

 2022android自定义文本路径选择器java教程_第2张图片

 2022android自定义文本路径选择器java教程_第3张图片

 2022android自定义文本路径选择器java教程_第4张图片

下面附上真正的完整代码:2022android自定义文本路径选择器java教程_第5张图片

 

2022android自定义文本路径选择器java教程_第6张图片

 

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

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
            mGetContent =
            registerForActivityResult(new ActivityResultContract() {
                                          @NonNull
                                          @Override
                                          public Intent createIntent(@NonNull Context context, Intent intent) {
                                              return intent;
                                          }

                                          @Override
                                          public Intent parseResult(int i, @Nullable Intent intent) {
                                              return intent;
                                          }
                                      },
                    new ActivityResultCallback() {
                        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);
                                }
                            }
                        }
                    });


    @Override
    protected 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() {
            @Override
            public void onClick(View view) {
                openBrowser();
            }
        });//打开系统存储

        findViewById(R.id.btn_open1).setOnClickListener(new View.OnClickListener() {
            @Override
            public 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)方法。
            /*Deprecated
                This method has been deprecated in favor of using the Activity Result API which brings increased
                type safety via an ActivityResultContract and the prebuilt contracts for common intents available in
                androidx.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-28D0
        Intent 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;
    }
    /*
    @Override
    protected 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() {
                @Override
                public 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);

    }

    @Override
    public void finish() {
        Intent intent = new Intent();
        intent.putExtra("file", filePath);
        setResult(RESULT_OK, intent);
        super.finish();
    }
    //重写finish函数,用putExtra方法向前一个activity传递filePath

    private void initView() {
        curPathTextView = (TextView) findViewById(R.id.curPath);
    }


    @Override
    public void onFileSelect(File selectedFile) {
        filePath = selectedFile.getPath();
    }

    @Override
    public void onDirSelect(File selectedDir) {
        filePath = selectedDir.getPath();
    }

    @Override
    public 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, getView

getCount : 要绑定的条目的数目,比如格子的数量
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 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为返回键的layout
        layoutReturnRoot = 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);//Bug
        listView.setOnItemClickListener(this);
         /**/
    }

    private class ViewHolder {
        TextView text;
        ImageView icon;
    }//存放用于显示的view

    public 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 类型的数组,返回的是该目录中的文件和目录。
            @Override
            public 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() {
            @Override
            public 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,RelativeLayout
        ViewHolder 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方法就可以了。
    */

    @Override
    public 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");
        }
    }

    @Override
    public 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文件是可以自己随便找的,就是用于显示些图标的文件。

2022android自定义文本路径选择器java教程_第7张图片

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



    
    

layout文件

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




    

    

        


            

activity_main.xml  用于展示主页面





    

    

file_item.xml  用于存放listview的子View




    

    


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




    

        

        

            

                
                

                
                

            

            

                

                

                

                

            

        


        
        

    

values文件

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


    Storage_path_select
    取消
    确定
    返回根目录
    上一级目录
    系统存储
    sd卡
    路径

manifest文件

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




    
    
    
    
    

    

        
            
                

                
            
        
        
    

 Gradle Scripts 文件

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

plugins {
    id 'com.android.application'
}

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.example.storage_path_select"
        minSdk 21
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility 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了,这样反而更慢。全局管理应该是个好东西,值得学一手。

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

你可能感兴趣的:(android,android,studio,ide)