《Android编程权威指南》项目三 MVVM架构实战解析

2020-04-09  本文已影响0人  小松与蘑菇

本文将通过一个BeatBox项目详细解析MVVM架构的使用以及android中主题样式的使用分析

@TOC

效果如图

在这里插入图片描述

每一个按钮都设置了指定的样式,每点击一个按钮都会发出相应的声音,如果对每一个按钮都进行设置的话,将非常繁琐,但是直接修改主题即可全部完成

项目结构

java和xml有9个主要文件(还有一些配置文件设定)


在这里插入图片描述

前期准备

<font color="red"><font color="red"> SingleFragmentActivity</font></font>是一个抽象类,因为我们所有的显示工作都在<font color="red">BeatBoxFragment</font>中完成,<font color="red"> BeatBoxActivity</font>仅仅作为创造<font color="red">BeatBoxFragment</font>的入口activity即可,他的一些通用操作继承<font color="red"><font color="red"> SingleFragmentActivity</font></font>

<font color="red"> SingleFragmentActivity</font>
public abstract class SingleFragmentActivity extends AppCompatActivity {
    protected abstract Fragment createFragment();

    protected int getLayoutResId() {
        return R.layout.activity_single_fragment;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_single_fragment);
        FragmentManager fragmentManager = getSupportFragmentManager();
        Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_container);
        if (fragment == null) {
            fragment = createFragment();
            fragmentManager.beginTransaction().add(R.id.fragment_container, fragment).commit();
        }
    }
}

这里就是连接他的资源id和创建Fragment管理器的通用功能,

<font color="red">activity_single_fragment
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

然后在<font color="red"><font color="red"> BeatBoxActivity</font></font>中只需要创建 <font color="red">BeatBoxFragment</font>的实例即可

<font color="red"> BeatBoxActivity</font>
public class  BeatBoxActivity extends  SingleFragmentActivity {

    @Override
    protected Fragment createFragment() {
        return BeatBoxFragment.newInstance();
    }
}

MVVM

接下来就是<font color="red">BeatBoxFragment</font>的工作了,他的作用是设计recyclerview,将一个一个的<font color="red">BeatBox</font>放入其中,并设置主题,每个<font color="red">BeatBox</font>包含一个<font color="red">sound</font>,这里我们用sound保存每一个音频的路径(资源都在assets文件夹中)文件名以及id,这个id是在BeatBox通过mAssetManager加载获得

<font color="red"> BeatBox
/**
 * 管理assets资源,创建Sound,维护Sound的集合
 */
public class BeatBox {
    //日志记录
    private static final String TAG = "BeatBox";
    private List<Sound> mSounds = new ArrayList<>();
    //音频播放池
    private static final int MAX_SOUNDS = 5;
    //存储资源目录
    private static final String SOUND_FOLDER = "sample_sounds";
    //访问assets的类
    private AssetManager mAssetManager;
    private SoundPool mSoundPool;
    public BeatBox(Context context) {
        mAssetManager = context.getAssets();
        //指定最大播放音频数,确定音频流类型,指定采样率
        mSoundPool = new SoundPool(MAX_SOUNDS, AudioManager.STREAM_MUSIC, 0);

        loadSounds();
    }

    /**
     * 播放音乐
     * @param sound
     */
    public void play(Sound sound) {
        Integer soundId = sound.getSoundId();
        if (soundId == null) {
            return;
        }
        mSoundPool.play(soundId, 1.0f, 1.0f, 1, 0, 1.0f);
    }

    public void relese() {
        mSoundPool.release();
    }
    /**
     * 加载文件夹里面的声音
     */
    private void loadSounds() {
        String [] soundNames;
        try {
            soundNames = mAssetManager.list(SOUND_FOLDER);//列出文件夹下所有的文件名
            Log.i(TAG, "loadSounds: " + soundNames.length + " sounds");
        } catch (IOException ioe) {
            Log.e(TAG, "loadSounds: could not list assets",ioe );
            return;
        }
        for (String filename : soundNames) {
            try {
                String assetPath = SOUND_FOLDER + "/" + filename;
                Sound sound = new Sound(assetPath);
                load(sound);  //每获得一个音频就将其加载
                mSounds.add(sound);
            } catch (IOException e) {
                Log.e(TAG, "loadSounds: "+filename,e );
            }

        }

    }

    /**
     * 用AssetFileDescriptor打开对应路径的音频,获得对应Id
     * @param sound
     * @throws IOException
     */
    private void load(Sound sound) throws IOException {
        AssetFileDescriptor assetFileDescriptor = mAssetManager.openFd(sound.getAssetPath());
        int soundId = mSoundPool.load(assetFileDescriptor, 1);
        sound.setSoundId(soundId);

    }
    public List<Sound> getSounds() {
        return mSounds;
    }
}
<font color="red"> Sound
public class Sound {
    private String mAssetPath;
    private String mName;
    private Integer mSoundId; //Sound Pool需要预加载音频,需要设置自己的ID

    public Integer getSoundId() {
        return mSoundId;
    }

    public void setSoundId(Integer soundId) {
        mSoundId = soundId;
    }
    /**
     * 获得wav名字并修改,资源名后缀为wav
     * @param assetPath
     */
    public Sound(String assetPath) {
        mAssetPath = assetPath;
        String[] components = assetPath.split("/");
        String filename = components[components.length - 1];
        mName = filename.replace(".wav", "");
    }

    public String getAssetPath() {
        return mAssetPath;
    }

    public String getName() {
        return mName;
    }
}

现在,出现了一个问题,试想一下,<font color="red">Sound</font>是model,假如我在View,也就是视图中,想要获得sound的内容怎么办?就像前面gif图中的,每个按钮上面都显示了音频的文件名。这个时候你可能会说,在<font color="red">BeatBoxFragment</font>中设置不就好了吗?这就是MVC模式,<font color="red">BeatBoxFragment</font>作为控制器链接M和V。
但是这导致<font color="red">Sound</font>出现在了<font color="red">BeatBoxFragment</font>中,Sound是具体的数据,而<font color="red">BeatBoxFragment</font>本应该负责对BeatBox的整个排布处理,BeatBox又是对Sound的各种操作,如播放等。所以如果<font color="red">Sound</font>出现在了<font color="red">BeatBoxFragment</font>中,那么将会打乱代码分工

所以,Sound和xml文件之间的数据传输应该有他们自己的联系通道,作为ViewModel,这就是MVVM模式
联系Sound文件的是list_item_sound.xml

<font color="red">list_item_sound.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" >
<!--    声明数据对象,使用数据绑定 ViewModel,这样就可以用@符号将数据填入布局中了
但是此时视图和模型没有真正联系,还需要在SoundHolder中添加绑定方法-->
    <data>
        <variable
            name="viewModel"
            type="com.example.BeatBox.SoundViewModel" />
    </data>
<!--    将按钮放在FrameLayout中,不论屏幕多大,拉伸的是框架,而不是按钮-->
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp">
        <Button
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:onClick="@{()->viewModel.onButtonClicked()}"
            android:text="@{viewModel.title}"
            tools:text="Sound name"/>
    </FrameLayout>

</layout>

这里将新建一个<font color="red"> SoundViewModel</font>类,而让他两连接起来。很简单,只需要上面代码中的data部分即可,不过你需要同时在build.gradle(app)里面设置可以进行数据绑定

android {
   compileSdkVersion 29
   buildToolsVersion "29.0.2"
   ……
   dataBinding{
       enabled=true
   }
}

还有一行代码值得注意

 android:onClick="@{()->viewModel.onButtonClicked()}"

进行viewModel绑定时特殊的写法,只需要在ViewModel中创建onButtonCilicked方法即可完成点击事件,无需监听器

然后创建

<font color="red"> SoundViewModel</font>
import androidx.databinding.BaseObservable;

/**
 * 为了让sound与布局文件联系,如果使用Fragment作为中转的话,必须要再定义一个专门针对Sound
 * 的fragment,这和Sound模型有冲突,所以定义这个ViewModel,来联系Sound和View
 */
public classSoundViewModel extends BaseObservable {
    private final String TAG = " SoundViewModel";
    private Sound mSound;
    private BeatBox mBeatBox;

    public SoundViewModel(BeatBox beatBox) {
        mBeatBox = beatBox;
    }

    public Sound getSound() {
        return mSound;
    }

    //获取sound的名字
    public String getTitle() {
        return mSound.getName();
    }
    public void setSound(Sound sound) {
        mSound = sound;
        notifyChange();//针对继承的BaseObservable,只要有更新就会通知绑定类
    }

    public void onButtonClicked() {
        mBeatBox.play(mSound);
//        Log.d(TAG, "onButtonClicked: 已点击播放"+mSound.getName());
    }
}

这个<font color="red"> SoundViewModel</font>的作用在于可以实时的将Sound的数据显示到list_item_sound.xml文件中,也可以让BeatBox开始播放音乐,完成这些方法,最后我们只需要在<font color="red">BeatBoxFragment</font>随便调用即可

核心完成代码

<font color="red">BeatBoxFragment</font>的布局文件如下,就是一个简单的recyclerview

<font color="red">fragment_beat_box.xml
<layout
    xmlns:android="http://schemas.android.com/apk/res/android">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</layout>

注意,这里我们使用的layout布局,他可以告诉数据绑定工具:“这个布局由你来处理”,同时默认生成了一个绑定类FragmentBeatBoxBinding,所以现在如果要实例化视图层级结构,就不用LayoutInflater了,只需实例化FragmentBeatBoxBinding类即可
他将以getRoot()方法引用整个布局,其他子布局将以android:id标签引用
比如对于fragment_beat_box.xml这个文件,getRoot()获得整个RecyclerView布局,而get_recycler_view()获得id名为recycler_view的布局,当然,在这里,他们是同一个布局

<font color="red">BeatBoxFragment</font>

万事俱备,我们来看看最后的代码
首先,创建数据和fragment,在这里获取BeatBox,此时的BeatBox里面有一个list,包含所有的sound,每个sound包含id,路径和文件名

 public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //fragment中的保护实例不被销毁的方法,所在的activity被销毁时,他将保留传给新的activity,解决设备旋转问题
        setRetainInstance(true);
        mBeatBox = new BeatBox(getActivity());
        Log.d(TAG, "onCreate: ");
    }

其次,创建视图

 @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        FragmentBeatBoxBinding binding= DataBindingUtil.inflate(inflater,R.layout.fragment_beat_box,container,false);
        binding.recyclerView.setLayoutManager(new GridLayoutManager(getActivity(),3));
        binding.recyclerView.setAdapter(new SoundAdapter(mBeatBox.getSounds()));
        Log.d(TAG, "onCreateView: ");
        return binding.getRoot();
    }

看见了吗,这里直接可以定义FragmentBeatBoxBinding 类,通过DataBindingUtil类获取到fragment_beat_box就完成了视图的实例化,很方便有木有!

然后给recyclerView设置布局和适配器,最后返回整个布局即可

接下来就是关键,适配器的设置

/**
     * 适配器获取每一个绑定的item,返回到SoundHolder
     */
    private class SoundAdapter extends RecyclerView.Adapter<SoundHolder>{
        private List<Sound> mSounds;
        public SoundAdapter(List<Sound> sounds) {
            mSounds = sounds;
        }
        @NonNull
        @Override
        public SoundHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            LayoutInflater inflater = LayoutInflater.from(getActivity());
            ListItemSoundBinding listItemSoundBinding = DataBindingUtil.inflate(inflater, R.layout.list_item_sound, parent, false);
            return new SoundHolder(listItemSoundBinding);
        }

        @Override
        public void onBindViewHolder(@NonNull SoundHolder holder, int position) {
            Sound sound = mSounds.get(position);
            holder.bind(sound);
        }

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

这里的适配器也是直接通过ListItemSoundBinding 类获取到list_item_sound.xml的实例,然后将其传到SoundHolder中

private class SoundHolder extends RecyclerView.ViewHolder {
        private ListItemSoundBinding mListItemSoundBinding;
        private SoundHolder(ListItemSoundBinding binding) {
            super(binding.getRoot());
            mListItemSoundBinding=binding;
            //在数据绑定对象中设置ViewModel,这样mListItemSoundBinding
            // 就通过ViewModel获得了BeatBox
            mListItemSoundBinding.setViewModel(new SoundViewModel(mBeatBox));
        }
        //更新新的sound数据
        public void bind(Sound sound) {
            mListItemSoundBinding.getViewModel().setSound(sound);
            mListItemSoundBinding.executePendingBindings();//强迫recyclerView刷新,更加流畅
        }
    }

在Soundholder中,mListItemSoundBinding就将mBeatBox放到新建的<font color="red"> SoundViewModel</font>中,让<font color="red"> SoundViewModel</font>可以操纵数据,这样list_item_sound就和SoundView联系起来,可以进行数据交互了

样式

最后还有关于为什么按钮全部变成一样的操作,在styles中,我们这样

<font color="red">styles.xml
<resources>
<!--    设置主题-->

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/red</item>
        <item name="colorPrimaryDark">@color/dark_red</item>
        <item name="colorAccent">@color/gray</item>
<!--        一直进入Theme.AppCompat,找到后得到这个属性,就是android界面的背景颜色-->
        <item name="android:windowBackground">@color/soothing_blue</item>
        <item name="buttonStyle">@style/BeatBoxButton</item>
    </style>

<!--    给按钮添加新样式-->
    <style name="BeatBoxButton" parent="Widget.AppCompat.Button">
        <item name="android:background">@drawable/button_beat_box</item>
    </style>
</resources>

这里是对主题进行修改
android:windowBackground就是设置整个窗口颜色为蓝色
buttonStyle就是设置按钮样式为BeatBoxButton
而我们在下面定义了BeatBoxButton样式的背景为button_beat_box
在drawable中

完整资源和代码文件在github中,有兴趣可以去看看

上一篇下一篇

猜你喜欢

热点阅读