Android开发纯按键文件浏览器

背景:几年前的工厂项目,教育平板外接遥控器,无触屏。需要像电脑文件夹一样一层一层浏览本地资源(音频课文),纯 DPAD 按键交互。代码是当时的写法,现在回看有不少粗糙之处,记录一下业务场景和后来的理解升级。

  • 硬件:低端 TV 盒子/,配红外遥控器,无触屏设备
  • 内容 :TF 卡 /听力 目录下,按年级分文件夹,叶子节点是 .mp3 或自定义加密音频
  • 核心诉求
    1. 上下键循环选中(到顶再按上回到底部)
    2. 确定键进入文件夹或播放
    3. 返回键回到上级目录,且焦点停留在刚才进入的那个文件夹上(这是 TV 交互的硬需求)
    4. 文件过滤:隐藏系统目录,按年级名排序

1、定义变量

ini 复制代码
TextView title; //标题
RecyclerView recyclerView; //列表
UnitAdapter mUnitAdapter; //适配器
ArrayList<HashMap<String, String>> unitList; //列表数据
LinearLayout ll_no_data; //无数据view
boolean isNoData = false; //无数据标识

String sDcardDir; //根目录名称
String cur_name; //当前目录名称
String cur_path; //当前目录路径
String last_title; //上次标题名称
int last_mPos = 0; //上次选择位置
int first_mPos = 0; //第一次点击位置

String root_name = ""; //一级目录名称
String title_str = "";

2、初始化和加载

scss 复制代码
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_list);

        title_str = getIntent().getStringExtra("title"); //传入的标题
        title = findViewById(R.id.title);
        if (!TextUtils.isEmpty(title_str)) {
            root_name = title_str;
        }
        title.setText(root_name);
        sDcardDir = SDCardUtils.getTfStorageDirectory(this) + "/听力"; //根目录路径

        recyclerView = findViewById(R.id.recyclerView);
        ll_no_data = findViewById(R.id.ll_no_data);
        title.setTextColor(getResources().getColor(R.color.col_1E2736));
        
        unitList = new ArrayList<>();
        mUnitAdapter = new UnitAdapter(this, new View.OnClickListener() {
            @Override
            public void onClick(View v) {// item点击
                mUnitAdapter.mPosSomeTime = ((int) v.getTag());
                Log.d("TAG", "((int) v.getTag()---> " + ((int) v.getTag()));
                mUnitAdapter.notifyDataSetChanged();
                ClickSelect(); //选择目录或文件
            }
        });
        recyclerView.setAdapter(mUnitAdapter);
        verifyStoragePermissions(this); //权限请求
    }

    /**
	 * 获取tf卡根目录
	 * @param context
	 * @return
	 */
	public static String getTfStorageDirectory(Context context) {
		String tfDir = null;
		StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
		Class<?> smc = sm.getClass();
		
		try {
			Method getPaths = smc.getMethod("getVolumePaths", new  Class[0]);
			String[] paths = (String[])getPaths.invoke(sm, new  Object[]{});
			if (paths.length >= 2) {
				tfDir = paths[1];
			}
		} catch (NoSuchMethodException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return tfDir;
	}

private static final int REQUEST_EXTERNAL_STORAGE = 1;
    private static String[] PERMISSIONS_STORAGE = {
            "android.permission.READ_EXTERNAL_STORAGE",
            "android.permission.WRITE_EXTERNAL_STORAGE"};

    //然后通过一个函数来申请
    public static void verifyStoragePermissions(Activity activity) {
        try {
            //检测是否有写的权限
            int permission = ActivityCompat.checkSelfPermission(activity,
                    "android.permission.WRITE_EXTERNAL_STORAGE");
            if (permission != PackageManager.PERMISSION_GRANTED) {
                // 没有写的权限,去申请写的权限,会弹出对话框
                ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
            }else{
                findData(sDcardDir,false); //查找目录
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

3、查找目录/文件 、排序并显示

less 复制代码
//搜索数据方法 传入目录 和 是否返回标识
private void findData(String sDcardDir, boolean isReturn) {
        if (new File(sDcardDir).exists()) {//文件存在
            Log.e("TAG", "222 path=" + sDcardDir);
            File[] files = new File(sDcardDir).listFiles();
            SearchFile(files);
            Log.e("TAG", "last_mPos=" + last_mPos);//上一个目录的索引
            if (isReturn){
                mUnitAdapter.mPosSomeTime = last_mPos;
                if(title.getText().toString().equals(root_name)){
                     mUnitAdapter.mPosSomeTime = first_mPos;
                }
            }else {
                mUnitAdapter.mPosSomeTime = 0;
            }
            Log.e("TAG", "mUnitAdapter.mPosSomeTime=" + mUnitAdapter.mPosSomeTime);
            if(unitList!=null && unitList.size()>0){
                ListSort(unitList);//排序
                mUnitAdapter.mCurPos = -1;
                mUnitAdapter.updateData(unitList); //刷新列表
                recyclerView.scrollToPosition(mUnitAdapter.mPosSomeTime);
            }else {
                isNoData = true;
            }
        } else {
            isNoData = true;
            toast("找不到相关文件");
        }
        //隐藏或显示  无数据view/列表
        ll_no_data.setVisibility(isNoData?View.VISIBLE:View.GONE);
        recyclerView.setVisibility(isNoData?View.GONE:View.VISIBLE);
    }

private void SearchFile(File[] files) { //搜索文件
        unitList.clear(); //清空上次搜索的文件列表
        for (File file : files) {
            if (file.isDirectory()) {
                if (file.getName().contains("XX") || file.getName().contains("语文")) {               
                     //自定义拦截条件 不显示这个文件
                      continue;
                    }
                HashMap<String, String> map = new HashMap();//创建文件对象
                if (file.getName().equals("英语"))) { //自定义显示文件名称
                    map.put("item", "XXX");
                } else {
                    if (file.getName().contains("_")) { //截取_
                        map.put("item", file.getName().substring(file.getName().indexOf("_") + 1));
                    } else {
                        map.put("item", file.getName());//文件名称
                    }
                }
                map.put("path_name", file.getPath());//文件路径
                map.put("file_type", "file");//文件夹类型
                unitList.add(map);//加入列表

            } else if (file.isFile()) {
                String path = file.getPath();
                String data_type = "";
                if (path.endsWith(".XXX")) {//查找指定扩展名的文件
                        data_type = "xxx";
                    } else if (path.endsWith(".AAA")) {
                        data_type = "aaa";
                    } else if (path.endsWith(".BBB")) {
                        data_type = "bbb";
                    } 

                if (!TextUtils.isEmpty(data_type)) {
                    HashMap<String, String> map = new HashMap();
                    map.put("item", file.getName()); 
                    map.put("path_name", file.getPath());
                    map.put("data_type", data_type);
                    map.put("file_type", "book");//具体文件类型  比如书本
                    unitList.add(map);
                }
            }
        }
        if (unitList == null || unitList.size() == 0) {
            toast("找不到相关文件");
            mUnitAdapter.updateData(unitList);
            isNoData = true;
        }else {
            isNoData = false;
        }
    }

//按年级排序
private ArrayList<HashMap<String, String>> ListSort(ArrayList<HashMap<String, String>> list){
        if(list.get(0).get("file_type").equals("file")){
            for (int i = 0; i < list.size(); i++) {
                String item=list.get(i).get("item");
                if (TextUtils.isEmpty(item)){
                    break;
                }
                if(item.contains("一年级") || item.contains("1年级") || item.contains("人民教育")){
                    list.get(i).put("index", "1");
                }else if(item.contains("二年级") || item.contains("2年级")){
                    list.get(i).put("index", "2");
                }else if(item.contains("三年级") || item.contains("3年级")){
                    list.get(i).put("index", "3");
                }else if(item.contains("四年级") || item.contains("4年级")){
                    list.get(i).put("index", "4");
                }else if(item.contains("五年级")  || item.contains("5年级")){
                    list.get(i).put("index", "5");
                }else if(item.contains("六年级") || item.contains("6年级")){
                    list.get(i).put("index", "6");
                }else if(item.contains("七年级") || item.contains("7年级")){
                    list.get(i).put("index", "7");
                }else if(item.contains("八年级") || item.contains("8年级")){
                    list.get(i).put("index", "8");
                }else if(item.contains("九年级") || item.contains("9年级")){
                    list.get(i).put("index", "9");
                }else {
                    list.get(i).put("index", (i+10)+"");
                }
            }

            Comparator<HashMap<String, String>> comparator = (details1, details2) -> {
                if (details1.get("index") != null && (Integer.parseInt(details1.get("index")) > Integer.parseInt(details2.get("index")))) {
                    return 1;
                } else if (details1.get("index") != null && (Integer.parseInt(details1.get("index")) < Integer.parseInt(details2.get("index")))) {
                    return -1;
                } else {
                    return 0;
                }
            };
            //这里就会自动根据规则进行排序
            Collections.sort(list, comparator);
        }

        return list;
    }

4、适配器内容

typescript 复制代码
public class UnitAdapter extends RecyclerView.Adapter<UnitAdapter.Holder> {
    public List<Map<String, String>> mDatas = new ArrayList();

    public void updateData(List<HashMap<String, String>> data) {
        mDatas.clear();
        mDatas.addAll(data);
        notifyDataSetChanged();
    }

    public int mPosSomeTime = 0;//临时选中

    private Context mContext;
    private View.OnClickListener mListener;

    public UnitAdapter(Context context, View.OnClickListener listener) {
        this.mContext = context;
        this.mListener = listener;
    }

    @NonNull
    @Override
    public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new Holder(LayoutInflater.from(mContext).inflate(R.layout.item, parent, false));

    }

    @Override
    public void onBindViewHolder(@NonNull Holder holder, int position) {
        holder.upateData(position);
    }

    @Override
    public int getItemCount() {
        return mDatas.size();
    }

    @Override
    public int getItemViewType(int position) {
       return super.getItemViewType(position);
    }

    public class Holder extends RecyclerView.ViewHolder {
        LinearLayout item_unit;
        TextView menuitem;
        ImageView iv_wjj; //文件夹图标

        public Holder(@NonNull View itemView) {
            super(itemView);
            item_unit = itemView.findViewById(R.id.item_unit);
            menuitem = itemView.findViewById(R.id.menuitem);
            iv_wjj = itemView.findViewById(R.id.iv_wjj);
            itemView.setOnClickListener(mListener);
        }

        public void upateData(int position) {
            itemView.setTag(position);
            String unit = mDatas.get(position).get("item");
            menuitem.setText(unit); //显示文件名称

            String type = mDatas.get(position).get("file_type");
            if(type != null && (type.equals("file"))){//根据类型显示隐藏文件夹图标
                iv_wjj.setVisibility(View.VISIBLE);
            }else {
                iv_wjj.setVisibility(View.GONE);
            }
            if(position == mPosSomeTime) {
                   item_unit.setBackgroundColor(mContext.getResources().getColor(R.color.col_666666));//选中的背景
                menuitem.setMarqueeRepeatLimit(Integer.MAX_VALUE);
                menuitem.setEllipsize(TextUtils.TruncateAt.MARQUEE);//文字跑马灯
                menuitem.setFocusableInTouchMode(true);
                menuitem.setHorizontallyScrolling(true);
                menuitem.setSelected(true);
            }else {
                item_unit.setBackgroundColor(mContext.getResources().getColor(R.color.bg_item));//普通背景
                menuitem.setSelected(false);

            }

        }
    }
}

5、item选择交互内容

csharp 复制代码
private void ClickSelect(){
        if (unitList == null || unitList.size() == 0) {
            return;
        }
        //文件类型
        String file_type = unitList.get(mUnitAdapter.mPosSomeTime).get("file_type");
        String item = unitList.get(mUnitAdapter.mPosSomeTime).get("item");
        String path_name = unitList.get(mUnitAdapter.mPosSomeTime).get("path_name");
        if ("file".equals(file_type)) { //文件夹
            last_title = title.getText().toString();
            if (last_title.equals(root_name)) {
                first_mPos = mUnitAdapter.mPosSomeTime;
            }
            cur_name = item; //记录当前选择名称
            cur_path = path_name;//记录当前选择的路径
            title.setText(cur_name);
            last_mPos = mUnitAdapter.mPosSomeTime;//记录选择的指针
            findData(path_name,false);//查找和显示下级目录
        }else if("book".equals(file_type)){ //书
            //数据类型
            String data_type=unitList.get(mUnitAdapter.mPosSomeTime).get("data_type");
            if (data_type.equals("xxx") || data_type.equals("aaa")) {
                Intent intent = new Intent(XXXActivity.this, XXXActivity.class);
                intent.putExtra("book", item); //名称
                intent.putExtra("path_name", path_name); //路径
                startActivity(intent); //跳转下一个页面
            } else if (data_type.equals("bbb")) {
                Intent intent = new Intent(XXXActivity.this, BBBActivity.class);
                intent.putExtra("book", item);
                intent.putExtra("path_name", path_name);
                startActivity(intent);
            }
        } 
    }

6、按键监听和交互 重点来了

csharp 复制代码
@Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_CENTER: //中间键/确定键
                if(unitList != null && unitList.size() > 0){
                    ClickSelect();
                }
                break;
            case KeyEvent.KEYCODE_DPAD_UP:   //向上键
//                Log.d("TAG","KEYCODE_DPAD_UP--->");
                if (mUnitAdapter.mPosSomeTime >= 0) {
                    mUnitAdapter.mPosSomeTime--;
                    if(mUnitAdapter.mPosSomeTime < 0){
                        //当前选中第一个时再按上会选中最后一个
                        mUnitAdapter.mPosSomeTime = unitList.size()-1;
                    }
                    recyclerView.scrollToPosition(mUnitAdapter.mPosSomeTime);
                    mUnitAdapter.notifyDataSetChanged();
                    return true; //必须return true
                }
                Log.d("TAG", "mUnitAdapter.mPosSomeTime---> " + mUnitAdapter.mPosSomeTime);
                break;
            case KeyEvent.KEYCODE_DPAD_DOWN:   //向下键
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
//                    Log.d("TAG","ACTION_DOWN--->");
                    if (mUnitAdapter.mPosSomeTime <= unitList.size() - 1) {
                        mUnitAdapter.mPosSomeTime++;
                        if(mUnitAdapter.mPosSomeTime > unitList.size()-1){
                            //当前选中最后一个时再按下会选中第一个
                            mUnitAdapter.mPosSomeTime = 0;
                        }
                        recyclerView.scrollToPosition(mUnitAdapter.mPosSomeTime);
                        mUnitAdapter.notifyDataSetChanged();
                    }
                    Log.d("TAG", "mUnitAdapter.mPosSomeTime---> " + mUnitAdapter.mPosSomeTime);
                    return true; //必须return true
                }
                break;
            case KeyEvent.KEYCODE_BACK:    //返回键
                if(unitList.size() > 0 ){
                    if(mUnitAdapter.mPosSomeTime < unitList.size()){
                        String path = unitList.get(mUnitAdapter.mPosSomeTime).get("path_name"); //获取当前路径
                        File file = new File(path);
                        Log.e("TAG", "file.getParent()---> " + file.getParent());//上级路径
                        if(file.getParent().equals(sDcardDir)){ //根目录直接返回
                            Log.e("TAG", "1111---> ");
                            break;
                        }else {
                            if(StorageUtil.isMountStorage(path)){
                                Log.e("TAG", "2222---> ");
                                if(new File(file.getParent()).getParent().equals(this.sDcardDir)){ //如果回到根目录显示原有标题
                                    title.setText(root_name);
                                }else {
                                    title.setText(last_title);
                                }
                                //查找并显示上级目录
                                findData(new File(file.getParent()).getParent(), true);
                                return true;   //这里由于break会退出,所以我们自己要处理掉 不返回上一层
                            }else {
                                break;
                            }
                        }
                    }
                }else {
                    if(cur_path != null && cur_path.length() > 0 && StorageUtil.isMountStorage(cur_path)){
                        Log.e("zcc", "3333---> ");
                        findData(new File(cur_path).getParent(), true);
                        title.setText(last_title);
                        return true;   //这里由于break会退出,所以我们自己要处理掉 不返回上一层
                    }else {
                        Log.e("zcc", "4444---> ");
                        break;
                    }
                }
        }
        return super.onKeyDown(keyCode, event);
    }


    /**
     * 是否挂载
     *
     * @param path
     * @return
     */
    public static boolean isMountStorage(String path) {
        return Environment.getStorageState(new File(path)).equals(Environment.MEDIA_MOUNTED);
    }

以上便是按键交互下的读取显示文件层级功能,可供参考

相关推荐
monkeyhlj1 小时前
Harness理解学习
java·人工智能·python·学习·ai编程
西凉的悲伤2 小时前
SpringBoot WebClient 介绍
java·spring boot·后端·webclient
Simon523142 小时前
mybatis执行流程、关联映射、注解开发
java·开发语言·mybatis
冷雨夜中漫步2 小时前
SQLite 深度解析:在 Java/Spring 中的使用与H2、Derby对比
java·spring·sqlite
laufing2 小时前
Java 模板引擎 FreeMarker 入门教程:语法、内建函数与常用案例
java·freemarker
wengqidaifeng2 小时前
C++从菜鸟到强手:2.类和对象(上)—— 从结构体到类的跨越
java·开发语言·c++
自律懒人2 小时前
2026年AI编程工具横评:Trae、Cursor、Claude Code、Copilot X,同一需求谁更强?
java·copilot·ai编程
夕除2 小时前
spring boot 13
java·mysql·spring
marlondu2 小时前
ScopedValue:Java 21 引入的结构化作用域值
java