android分类布局 android 分类列表_ide


添加权限和相关依赖

// ViewModel and LiveData
    implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
    //汉字转换拼音
    implementation 'com.belerweb:pinyin4j:2.5.1'
    //图片加载
    implementation 'com.github.bumptech.glide:glide:4.11.0'
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

SideBar

如果音乐按照像图片那样按照日期分类,会不好查找,所以想使用类似联系人那样按照音乐名的首字母进行分类排序,这个可以在网上找到很多类似的,我使用的是下面的这个。
https://github.com/AlexLiuSheng/AnimSideBar 可以使用 gradle 直接引用,但代码不多,就直接 copy 过来了,灵活性更高,容易自己实现自己的需求
首先自定义一个 SideBar 组件

public class SideBar extends AppCompatTextView {
    public static String[] letters = new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "I",
            "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
            "W", "X", "Y", "Z", "#"};

    // 画笔
    private Paint textPaint;
    private Paint bigTextPaint;

    private ISideBarSelectCallBack callBack;

    //事件触碰时的y轴坐标
    private float eventY;
    private float w;
    private float sideTextWidth;

    //是否重新测量宽高
    private boolean isTouching = false;
    //一个字母的高度
    private float itemH;

    //振幅
    private float A = dp(50);
    //波峰与bigText之间的距离
    private int gapBetweenText = dp(10);
    //开口数量
    private int openCount = 5;
    //字体缩放,基于textSize
    private float fontScale = 1;
    //大字大小
    private float bigTextSize;

    public SideBar(@NonNull Context context) {
        super(context);
        init(null);
    }

    public SideBar(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

    public SideBar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    public SideBar setFontScale(float fontScale){
        this.fontScale = fontScale;
        return this;
    }

    public void setDataResource(String[] data) {
        letters = data;
        invalidate();
    }

    public void setOnStrSelectCallBack(ISideBarSelectCallBack callBack) {
        this.callBack = callBack;
    }

    public SideBar setBigTextSize(float bigTextSize) {
        this.bigTextSize = bigTextSize;
        bigTextPaint.setTextSize(bigTextSize);
//        invalidate();
        return this;
    }

    public SideBar setA(float a) {
        A = a;
//        invalidate();
        return this;
    }

    public SideBar setGapBetweenText(int gapBetweenText) {
        this.gapBetweenText = gapBetweenText;
//        invalidate();
        return this;
    }

    public SideBar setOpenCount(int openCount) {
        this.openCount = openCount;
//        invalidate();
        return this;
    }

    private void caculateAW(int height) {
        // 计算单个字母高度,-2为了好看
        itemH = (height) * 1.0f / letters.length;
        // 开口宽度
        float opendWidth = itemH * openCount;
        //角速度 2PI/t 周期
        w = (float) (Math.PI * 2.0f / (opendWidth * 2));
    }

    private int dp(int v) {
        final float scale = getContext().getResources().getDisplayMetrics().density;
        return (int) (v * scale + 0.5f);
    }

    // 初始化
    private void init(AttributeSet attrs){
        if (attrs != null) {
            TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.SideBar);
            A = dp(typedArray.getInteger(R.styleable.SideBar_A, 100));
            fontScale = typedArray.getFloat(R.styleable.SideBar_fontScale, 1);
            bigTextSize = typedArray.getFloat(R.styleable.SideBar_bigTextSize, getTextSize() + 2);
            gapBetweenText = dp(typedArray.getInteger(R.styleable.SideBar_gapBetweenText, 50));
            openCount = typedArray.getInteger(R.styleable.SideBar_openCount, 13);
        } else {
            bigTextSize = getTextSize() + 2;
        }
        // 设置画笔
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(getCurrentTextColor());
        textPaint.setTextSize(getTextSize());
        textPaint.setTextAlign(Paint.Align.CENTER);

        bigTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        bigTextPaint.setColor(getCurrentTextColor());
        bigTextPaint.setTextSize(bigTextSize);
        bigTextPaint.setTextAlign(Paint.Align.CENTER);


        float sideTextHeight = textPaint.getFontMetrics().descent - textPaint.getFontMetrics().ascent;
        sideTextWidth = textPaint.measureText("W");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //计算一边隆起的数量
        int singleSideCount = openCount / 2;
        //如果过已经触碰,且触碰的y坐标在屏幕范围内,index 就等与(触碰的y坐标值 / 每个字母的高多值)<==> 触碰的字母下标值
        int index = isTouching && eventY >= 0 && eventY <= getMeasuredHeight() ? (int) Math.floor((eventY / itemH)) : -(singleSideCount + 1);
//        index=Math.min(letters.length,index);
//        CLog.e("index:" + index + "eventY:" + eventY);
        //sidebar x轴中心点到右边边框的距离
        float sideX = sideTextWidth / 2 + getPaddingRight();
        // 画每一个字母
        for (int i = 0; i < letters.length; i++) {
            //rest textsize
            textPaint.setTextSize(getTextSize());
            int y = (int) (itemH * (i + 1));
            int x;
            if (Math.abs(i - index) > singleSideCount) { //要画字母的 y 轴位置不在弧度隆起的范围内
                x = (int) (getMeasuredWidth() - sideX);
            } else { //计算弧度范围内的字母坐标
                float percent = eventY / itemH;
                int t = (int) (i * itemH - eventY);
                double v = A * Math.sin(w * t + Math.PI / 2);

				//如果算出来小于字体宽度 就取字体宽度
                v = Math.max(v, sideX);
                x = (int) (getMeasuredWidth() - v);
                //根据delta缩放字体
                if (v == sideX) {
                    textPaint.setTextSize(getTextSize());
                } else {
                    float delta = (Math.abs((i - percent)) / singleSideCount);
                    float textSize = getTextSize() + (1 - delta) * getTextSize() * fontScale;
//                    textSize=Math.max(textSize,getTextSize());
                    textPaint.setTextSize(textSize);
                }

            }
            // 画出字母
            canvas.drawText(letters[i], x, y, textPaint);
        }
        //判断是否有触碰的 字母 item,有就画出大字母图标
        if (index != -(singleSideCount + 1)) {
            index = Math.max(index, 0);
            // 会有超出界限的问题,
            index = Math.min(index, letters.length-1);
            // 画出最大的那个字母
            canvas.drawText(letters[index], getPaddingLeft() + getBigTextWidth() / 2, (int) (itemH * (index + 1)), bigTextPaint);
            if (callBack != null) {
                // 将触碰的字母坐标,和字母回掉给Activity界面
                callBack.onSelectStr(index, letters[index]);
            }
        }
    }

    // 获得字母的宽度,这里获得 W 字母的宽度
    private float getBigTextWidth() {
        return bigTextPaint.measureText("W");
    }

    /**
     * 触碰事件
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int startTouchX = (int) (getMeasuredWidth() - A);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: //第一个触点按下之后
            case MotionEvent.ACTION_MOVE: //当触点在屏幕上移动时触发,触点在屏幕上停留也是会触发的,主要是由于它的灵敏度很高,而我们的手指又不可能完全静止(即使我们感觉不到移动,但其实我们的手指也在不停地抖动)
                if (event.getX() > startTouchX) {
                    eventY = event.getY();
                    if (!isTouching) {
                        isTouching = true;
                        requestLayout();
                    } else {
                        invalidate();
                    }

                } else {
                    if (isTouching) {
                        resetDefault();
                    }
                }
                return true;
            case MotionEvent.ACTION_CANCEL: //不是由用户直接触发,由系统在需要的时候触发,例如当父view通过使函数onInterceptTouchEvent()返回true,从子view拿回处理事件的控制权时,就会给子view发一个ACTION_CANCEL事件,子view就再也不会收到后续事件
            case MotionEvent.ACTION_UP: //当触点松开时被触发
                resetDefault();
                return true;

        }
        return super.onTouchEvent(event);
    }

    private void resetDefault() {
        isTouching = false;
        eventY = 0;
        requestLayout();
    }

	//设置自定义view大小
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int viewWidth = MeasureSpec.getSize(widthMeasureSpec);
        caculateAW(MeasureSpec.getSize(heightMeasureSpec));
        if (mode == MeasureSpec.UNSPECIFIED || mode == MeasureSpec.AT_MOST) {
            viewWidth = !isTouching ? (int) (sideTextWidth + getPaddingLeft() + getPaddingRight()) : (int) (A + gapBetweenText + getBigTextWidth() + getPaddingLeft() + getPaddingRight());
        }
//        CLog.e("width:" + viewWidth + "height:" + MeasureSpec.getSize(heightMeasureSpec));
        setMeasuredDimension(viewWidth, MeasureSpec.getSize(heightMeasureSpec));
    }

    public interface ISideBarSelectCallBack {
        void onSelectStr(int index,String selectStr);
    }
}

重点是 onDraw() 方法

这里出现了问题,有时候数组会越界,所以在前面添加判断代码,如果越界就等于数组最大值

android分类布局 android 分类列表_android_02


在 res/values 文件夹下建立 attr.xml 文件,定义5个自定义的属性,这5个属性对应 SideBar 中的几个属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SideBar">
        <attr name="fontScale" format="float" />
        <attr name="bigTextSize" format="float" />
        <attr name="openCount" format="integer" />
        <attr name="A" format="integer" />
        <attr name="gapBetweenText" format="integer" />
    </declare-styleable>
</resources>

在 init() 方法中通过 TypedArray 类获得这些属性

android分类布局 android 分类列表_Math_03

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:sidebar="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="16dp">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/yinyue_recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.zwt.zwttransmit.fragment.SideBar
        android:id="@+id/yinyue_sidebar"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_alignParentEnd="true"
        android:paddingEnd="10dp"
        android:textColor="@color/colorAccent"
        android:textSize="12sp"
        sidebar:A="40"
        sidebar:gapBetweenText="50"
        sidebar:openCount="5"
        sidebar:fontScale="1"/>

</RelativeLayout>

RecyclerView 用于放音乐数据

public class YinYueFragment extends Fragment {

    private View view;
    private SideBar sideBar;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_yinyue, container,false);
        initView();
        return view;
    }

    private void initView(){
        sideBar = (SideBar)view.findViewById(R.id.yinyue_sidebar);
        sideBar.setOnStrSelectCallBack(new SideBar.ISideBarSelectCallBack() {
            @Override
            public void onSelectStr(int index, String selectStr) {
                // 回掉,当点击 sidbar 的时候会被调用,index 表示点击的位置,selectStr 表示点击的字母u
              
            }
        });
    }

    @Override
    public void onStart() {
        super.onStart();
    }

    public static YinYueFragment newInstance() {
        YinYueFragment fragment = new YinYueFragment();
        return fragment;
    }
}

这里用的是Fragment ,和 Activity 是一样的 onCreateView 就相当于 Activity 的 onCreate() 函数

android分类布局 android 分类列表_android分类布局_04

获取所有的音乐数据

建立实体类

public class Music {
    // 歌曲名
    private String name;
    // 路径
    private String path;
    // 专辑
    private String album;
    //专辑图片地址
    private String albumArt;
    // 作者
    private String artist;
    // 大小
    private String size;
    // 时长
    private int duration;
    //日期
    private long time;
    //首字母
    private String pinyin;
    // 音乐文件类型
    private String contentType;

    public Music(){}

    public Music(String name, String path, String album, String albumArt, String artist, String size, int duration, long time, String pinyin, String contentType) {
        this.name = name;
        this.path = path;
        this.album = album;
        this.albumArt = albumArt;
        this.artist = artist;
        this.size = size;
        this.duration = duration;
        this.time = time;
        this.pinyin = pinyin;
        this.contentType = contentType;
    }
    public String getDate() {
        return new SimpleDateFormat("yyyy/MM/dd a hh:mm:ss")
                .format(new Date(time*1000L));
    }
    // 省略 getter 和 setter 方法
}

LiveData+ViewModel 获取数据

public class MusicViewModel extends ViewModel {

	//用于存放音乐数据
    Map<String, ArrayList<Music>> musicMap = new HashMap<>();

    private MutableLiveData<Map<String, ArrayList<Music>>> musicLiveData;

    String[] STORE_IMAGES = {
            MediaStore.Audio.Media.TITLE,//歌曲名
            MediaStore.Audio.Media.MIME_TYPE,//歌曲类型
            MediaStore.Audio.Media.ALBUM,//专辑
            MediaStore.Audio.Media.ALBUM_ID, //专辑ID,用于查找 专辑图片
            MediaStore.Audio.Media.ARTIST,//作者
            MediaStore.Audio.Media.DATA,//路径
            MediaStore.Audio.Media.DURATION,//时长
            MediaStore.Audio.Media.SIZE, // 大小
            MediaStore.Audio.Media.DATE_ADDED //放入数据库日期

    };

    public MutableLiveData<Map<String, ArrayList<Music>>> getMusicLiveData() {
        if (musicLiveData == null) {
            musicLiveData = new MutableLiveData<Map<String, ArrayList<Music>>>();
            loadMusic();
        }
        return musicLiveData;
    }

    private void loadMusic(){
        new Thread(new Runnable() {
            @Override
            public void run() {
            	// 初始化Map
                initMap();
                Cursor cursor = MyApplication.getContext().getContentResolver().query(
                        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                        STORE_IMAGES,
                        null,
                        null,
                        null);

                if(cursor.moveToNext()){
                    int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
                    int albumIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM);
                    int artistIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
                    int dataIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);
                    int durationIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION);
                    int sizeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE);
                    int timeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATE_ADDED);
                    int albumIdIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID);
                    int mimeTypeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE);

                    do{
                        String title = cursor.getString(titleIndex);
                        String album = cursor.getString(albumIndex);
                        String artist = cursor.getString(artistIndex);
                        String path = cursor.getString(dataIndex);
                        int duration = cursor.getInt(durationIndex);
                        long size = cursor.getLong(sizeIndex);
                        long time = cursor.getLong(timeIndex);
                        int albumId = cursor.getInt(albumIdIndex);
                        String mimeType = cursor.getString(mimeTypeIndex);
						
						//判断音乐类型
                        if ("audio/mpeg".equals(mimeType.trim())) {
                            mimeType = "mp3";
                        } else if ("audio/x-ms-wma".equals(mimeType.trim())) {
                            mimeType = "wma";
                        }
                        //根据专辑id(albumId)获取音乐专辑图片地址
                        String albumArt = getAlbumArt(albumId);
                        //获得音乐名称首字母
                        String pinyin = getPinyin(title);
						//将音乐文件大小转换成MB格式
                        String sizeStr = String.valueOf(size / 1024f / 1024f).substring(0, 4) + "MB";

                        Music music = new Music(title, path, album, albumArt, artist, sizeStr, duration, time, pinyin, mimeType);
						//根据音乐名称首字母 放入map
                        musicMap.get(pinyin).add(music);

                    }while (cursor.moveToNext());
                }
                musicLiveData.postValue(musicMap);
                cursor.close();

            }
        }).start();
    }

	//初始化map
    private void initMap(){
        for(String s: SideBar.letters){
            musicMap.put(s,new ArrayList<Music>());
        }

    }

	/**
	* 根据专辑的id查找专辑的图片
	* return:专辑图片地址
	*/
    private String getAlbumArt(int album_id) {
        String mUriAlbums = "content://media/external/audio/albums";
        String[] projection = new String[]{"album_art"};
        Cursor cur = MyApplication.getContext().getContentResolver().query(Uri.parse(mUriAlbums + "/" + Integer.toString(album_id)), projection, null, null, null);
        String album_art = "";
        if (cur.getCount() > 0 && cur.getColumnCount() > 0) {
            cur.moveToNext();
            album_art = cur.getString(0);
        }
        cur.close();
        return album_art;
    }

	/**
	* 获得字符串首字母
	* return 返回字符串首字母
	*/
    private String getPinyin(String str){
        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
        format.setCaseType(HanyuPinyinCaseType.UPPERCASE);//设置为大写形式
        format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);// 不用加入声调

        if (str == null || str.equals(""))
            return "#";
        try {
            char input = str.trim().charAt(0);//拿到第一个字
            if (isHanZi(input)) {// 如果是汉字
                //返回一个字符串数组是因为该汉字可能是多音字,此处只取第一个结果
                return PinyinHelper.toHanyuPinyinStringArray(input, format)[0].substring(0,1);
            }else{
                if(isEnglish(input)){ // 如果是英文
                    return String.valueOf(input).toUpperCase(Locale.ENGLISH);
                }else {
                    return "#";
                }
            }
        } catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) {
            badHanyuPinyinOutputFormatCombination.printStackTrace();
        }
        return "#";
    }
    //判断是否是汉字
    private boolean isHanZi(char c){
        if((c >= 0x4e00)&&(c <= 0x9fbb)) {
            return true;
        }
        return false;
    }
    //判断是否是英文
    private boolean isEnglish(char c){
        return Character.isLowerCase(c) || Character.isUpperCase(c);
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        // 释放资源
    }
}

与图片数据获取类似,没什么好说的

显示界面

与图片相同,嵌套 RecyclerView 采用方法,
music_group_item.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/music_item_cardView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:cardCornerRadius="4dp"
    app:cardElevation="0dp">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/music_group_item_recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </LinearLayout>

</androidx.cardview.widget.CardView>

music_child_item.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/music_item_cardView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_margin="2dp"
    app:cardCornerRadius="4dp"
    app:cardElevation="0dp">
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <ImageView
            android:id="@+id/music_item_imageView"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_gravity="center"
            android:layout_alignBottom="@+id/linearLayout"
            android:layout_marginStart="11dp"
            android:layout_toEndOf="@+id/list_column_v"
            android:src="@drawable/img_head_portrait" />
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@id/music_item_imageView"
            android:orientation="vertical"
            android:layout_marginTop="5dp"
            android:layout_marginLeft="15dp"
            android:id="@+id/linearLayout">

            <TextView
                android:id="@+id/music_item_textView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="17sp"
                android:textColor="#000000"
                android:text="Something just like this" />

            <TextView
                android:id="@+id/music_item_textView_information"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="5dp"
                android:textSize="12sp"
                android:text="Cold Player/The Chainsmokers" />
        </LinearLayout>
    </LinearLayout>
</androidx.cardview.widget.CardView>

添加适配

public class MusicAdapter extends RecyclerView.Adapter<MusicAdapter.ViewHolder> {

    private Context context;
    private Map<String,ArrayList<Music>> musicGroupMap;

    public MusicAdapter(Map<String,ArrayList<Music>> musicGroupList) {
        this.musicGroupMap = musicGroupList;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if (context == null){
            context = parent.getContext();
        }
        View view = LayoutInflater.from(context).inflate(R.layout.music_group_item, parent, false);
        final MusicAdapter.ViewHolder holder = new MusicAdapter.ViewHolder(view);
        return holder;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        ArrayList<Music> list = musicGroupMap.get(SideBar.letters[position]);
        MusicAdapter.ChildAdapter childAdapter = (MusicAdapter.ChildAdapter) holder.recyclerView.getAdapter();

        if (childAdapter == null) {
            GridLayoutManager manager = new GridLayoutManager(context, 1);

            holder.recyclerView.setLayoutManager(manager);

            holder.recyclerView.setAdapter(new MusicAdapter.ChildAdapter(list));
        }else {
            childAdapter.setData(list);
            childAdapter.notifyDataSetChanged();
        }
    }

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

    static class ViewHolder extends RecyclerView.ViewHolder{
        CardView cardView;
        RecyclerView recyclerView;
        public ViewHolder(View view){
            super(view);
            cardView = (CardView)view;
            recyclerView = (RecyclerView) view.findViewById(R.id.music_group_item_recyclerView);
        }
    }

    /****************ChildAdapter*************************/

    public class ChildAdapter extends RecyclerView.Adapter<MusicAdapter.ChildAdapter.ChildViewHolder>{

        private ArrayList<Music> musicList;

        public ChildAdapter(ArrayList<Music> musicList) {
            this.musicList = musicList;
        }

        public void setData(ArrayList<Music> musicList) {
            this.musicList = musicList;
        }

        @NonNull
        @Override
        public MusicAdapter.ChildAdapter.ChildViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            if (context == null){
                context = parent.getContext();
            }
            View view = LayoutInflater.from(context).inflate(R.layout.music_child_item, parent, false);
            final MusicAdapter.ChildAdapter.ChildViewHolder holder = new MusicAdapter.ChildAdapter.ChildViewHolder(view);
            return holder;
        }

        @Override
        public void onBindViewHolder(@NonNull MusicAdapter.ChildAdapter.ChildViewHolder holder, int position) {
            Music music = musicList.get(position);
            holder.textView.setText(music.getName()+"."+music.getContentType());
            Glide.with(context).load(music.getAlbumArt()).into(holder.imageView);
            holder.textViewInformation.setText(music.getSize()+ "  |  "+music.getDate());
        }

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

        class ChildViewHolder extends RecyclerView.ViewHolder{

            CardView cardView;
            TextView textView;
            ImageView imageView;
            TextView textViewInformation;
            public ChildViewHolder(View view){
                super(view);
                cardView = (CardView)view;
                textView = (TextView) view.findViewById(R.id.music_item_textView);
                imageView = (ImageView) view.findViewById(R.id.music_item_imageView);
                textViewInformation = (TextView) view.findViewById(R.id.music_item_textView_information);
            }
        }
    }
}

没什么好说的与图片类似

将适配设置到界面

public class YinYueFragment extends Fragment {

    private RecyclerView recyclerView;

    private MusicAdapter adapter;// 适配器

    private View view;
    private SideBar sideBar;

    //目标项是否在最后一个可见项之后
    private boolean mShouldScroll;
    // 记录目标项位置
    private int mToPosition;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_yinyue, container,false);
        initView();
        return view;
    }

    private void initView(){
		// 设置 SideBar 
        sideBar = (SideBar)view.findViewById(R.id.yinyue_sidebar);
        sideBar.setOnStrSelectCallBack(new SideBar.ISideBarSelectCallBack() {
            @Override
            public void onSelectStr(int index, String selectStr) {
                // 点击 SideBar 回掉 
                smoothMoveToPosition(index);
            }
        });
		// 实例化 RecyclerView
        recyclerView = (RecyclerView)view.findViewById(R.id.yinyue_recyclerView);
        final GridLayoutManager layoutManager = new GridLayoutManager(view.getContext(), 1);
        recyclerView.setLayoutManager(layoutManager);
        // 添加滚动事件监听器
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (mShouldScroll) {
                    mShouldScroll = false;
                    smoothMoveToPosition(mToPosition);
                }
            }
        });

        /**********获得数据**************/

        final MusicViewModel model = ViewModelProviders.of(this).get(MusicViewModel.class);

        model.getMusicLiveData().observe(this, new Observer<Map<String,ArrayList<Music>>>() {

            @Override
            public void onChanged(final Map<String,ArrayList<Music>> musicMap) {

                recyclerView.addItemDecoration(new SectionNewDecoration(new SectionNewDecoration.OnTagListener(){

                    @Override
                    public String getTag(int position) {
                        if(musicMap.get(SideBar.letters[position]).size()>0) {
                            return SideBar.letters[position];
                        }
                        return "-1";
                    }
                }));

                adapter = new MusicAdapter(musicMap);
                recyclerView.setAdapter(adapter);

            }
        });

    }

    @Override
    public void onStart() {
        super.onStart();
    }

    public static YinYueFragment newInstance() {
        YinYueFragment fragment = new YinYueFragment();
        return fragment;
    }
    // 将RecyclerView滚动到相应的位置
    private void smoothMoveToPosition(final int position) {
        // 第一个可见位置
        int firstItem = recyclerView.getChildLayoutPosition(recyclerView.getChildAt(0));
        // 最后一个可见位置
        int lastItem = recyclerView.getChildLayoutPosition(recyclerView.getChildAt(recyclerView.getChildCount() - 1));

        if (position < firstItem) {
            // 如果跳转位置在第一个可见位置之前,就smoothScrollToPosition可以直接跳转
            recyclerView.smoothScrollToPosition(position);
        } else if (position <= lastItem) {
            // 跳转位置在第一个可见项之后,最后一个可见项之前
            // smoothScrollToPosition根本不会动,此时调用smoothScrollBy来滑动到指定位置
            int movePosition = position - firstItem;
            if (movePosition >= 0 && movePosition < recyclerView.getChildCount()) {
                int top = recyclerView.getChildAt(movePosition).getTop();
                recyclerView.smoothScrollBy(0, top);
            }
        } else {
            // 如果要跳转的位置在最后可见项之后,则先调用smoothScrollToPosition将要跳转的位置滚动到可见位置
            // 再通过onScrollStateChanged控制再次调用smoothMoveToPosition,执行上一个判断中的方法
            recyclerView.smoothScrollToPosition(position);
            mToPosition = position;
            mShouldScroll = true;
        }
    }
}

最后实现想要的效果