背景:几年前的工厂项目,教育平板外接遥控器,无触屏。需要像电脑文件夹一样一层一层浏览本地资源(音频课文),纯 DPAD 按键交互。代码是当时的写法,现在回看有不少粗糙之处,记录一下业务场景和后来的理解升级。
- 硬件:低端 TV 盒子/,配红外遥控器,无触屏设备
- 内容 :TF 卡
/听力目录下,按年级分文件夹,叶子节点是.mp3或自定义加密音频 - 核心诉求:
-
- 上下键循环选中(到顶再按上回到底部)
- 确定键进入文件夹或播放
- 返回键回到上级目录,且焦点停留在刚才进入的那个文件夹上(这是 TV 交互的硬需求)
- 文件过滤:隐藏系统目录,按年级名排序
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);
}
以上便是按键交互下的读取显示文件层级功能,可供参考