音乐播放器的实现过程及四大组件入门

前言

刚入行安卓的萌新一枚,目前对工作内容,工作方向还处于混沌状态,搞个Android Studio和一个简单的app都搞了四天,期间的一些问题也缺少一套方法论去解决,完全是面向百度,没有解决问题的思路,写个博客总结一下这几天的一些坑和遇到的问题。用一个写的乱七八糟的demo去由浅入深稍微熟悉一下安卓四大组件,以及一个项目从开发到打包成apk的过程。

乱七八糟的Demo

本Demo写的时候属于是意识流写法,并没有明确的思路(软件工程还给老师了),现在重新学着规范,写一下文档。

项目要求

实际上是自己临时想的一些功能,似乎网上也有比较完善的实现代码。最初的设想是能具备以下功能:

  • 一款能够设置时间,定时打开手机钉钉打卡的app;
  • 能够自己设置时间,并且可以选择一天开启还是长期开启
  • 能够在设置中关闭定时开启的功能
  • 拓展功能,可以定时打开任意手机的app,通过安卓自带的一些访问手机应用程序的接口来实现此功能
    最后的结果是,失败了,虽然看上去很简单,或许实际也很简单,但是开发过程总是遇到各种问题,然后最重要的一个功能始终无法实现,暂时不了了之了,以后再来重新做吧(悲。文末会总结一下开发这个功能时候遇到的问题。现在先说说真正实现的功能。
    于是退而求其次,我选择用一个音乐播放器demo来研究四大组件。音乐播放器功能:
  • 播放,暂停,上下首
  • 显示歌曲信息(未实现,但是会贴上实现的代码,从别人完成的代码吸取经验)
  • 添加本地音乐(以后再实现吧。)

软件可行性分析

选择完项目之后的第一步,就是可行性分析。可行性分析包含四个要点:技术可行性,经济可行性,操作可行性,法律可行性。

  • 技术可行性,播放暂停上下首都是简单的状态切换判断功能,显示歌曲信息何添加本地音乐等都是跟数据库的一些操作有关,也是可以实现的。编程语言方面,我使用了Java,因为Kotlin不会。
  • 经济可行性:不要钱,所以从经济层面将是可以实行的。
  • 操作可行性:供用户操作的接口按钮实际上只有切歌,暂停,还有一个添加本地音乐。以从用户操作层面上来说,此次项目开发是可行的。
  • 法律可行性:没有盈利,仿的也是开源代码,但是有杰伦的歌(没有版权。

需求分析

项目要求已经在上面提到了
下面画一下简单的uml类图
在这里插入图片描述

概要设计

实现音乐播放部分的很简单,基本上就是MusicActivity和MyService实现的。MusicActivity利用广播对Myservice的行为进行控制。MyService实际上对音乐进行播放暂停等操作,由于实际的显示界面是由MusicActiviy展示的,所以如果需要对歌曲信息等进行展示,还需要MysService的广播将自己的歌曲信息以及状态反馈给MusicActivity。其具体实现过程放到后文讲。这一部分是音乐播放的功能,没有涉及到数据库操作,希望后续熟悉后能加入。这是音乐播放部分。
此外虽然没有完成但是依然保留的定时启动钉钉部分,主界面的按钮可以跳转到ClockActivity,具体是通过安卓自带的TimePicker来实现闹钟定时功能,然后通过pingIntent来传输给静态广播需要打开某app的请求。由于不知名原因,定时和启动功能均未能正确运行。
至此,安卓四大组件其三以及提到过了,分别是:

  • Activity(活动)
  • Service(服务)
  • Broadcast Receiver(广播 接收者)
    我们还未提到的第四大组件是content provider(内容提供者)

四大组件详解

在讲四大组件前先来看一下安卓架构
以下许多内容都是参考大佬的博客[点击跳转]干货很多(https://blog.csdn.net/joye123/article/details/116197862?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_utm_term~default-1-116197862-blog-91946872.pc_relevant_multi_platform_whitelistv3&spm=1001.2101.3001.4242.2&utm_relevant_index=4)

在这里插入图片描述
可以看到四大组件是属于应用程序框架层。而开发者进行安卓app开发基本上都是基于四大组件进行开发。
Activity:Activity是开发中最常用的,也是最复杂的一个组件。它是用户可以专注做一些事情的东西。它的主要功能就是可以和用户进行交互操作,所以几乎所有的Activity都会负责创建一个显示窗口,然后通过setContentView显示特定的UI。

Service:除了Activity,Service是第二复杂的组件。和Activity相比,Service是一种处于后台长时间运行的组件,它没有UI界面,不需要与用户交互。它被设计用来后台执行耗时任务或者为其他应用程序提供功能调用的服务。

BroadcastReceiver:广播接收者,这个组件比较简单,比较好理解了。类似于观察者模式,应用程序可以添加自己关心的广播事件,从而为用户提供更好的使用体验。这些广播可以是来自于操作系统、其他的应用程序、甚至是自己的应用程序。例如网络连接变化、电池电量变化、开机启动等。

ContentProvider:内容提供者,它被设计用来在不同的应用程序之间共享数据。例如电话程序中的联系人数据,就可以被其他应用程序读取。如果仅仅是在同一个程序中存取数据的话,用SQLiteDatabase接口就可以了。

系统管理四大组件

除了BroadcastReceiver可以动态注册外,四大组件在使用之前必须先在 AndroidManifest.xml清单文件中进行声明。我们这个demo中ClockActivity用的就是静态注册,需要在AndroidManifest.xml中声明,即使app关闭了,仍然可以在后台工作。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.note">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!-- 查询联系人 -->
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <!-- 添加联系人 -->
    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
    <uses-permission
        android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
        tools:ignore="ProtectedPermissions" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Note"
        tools:targetApi="31">
        <activity
            android:name=".MessageActivity"
            android:exported="false" />

        <receiver
            android:name=".receiver.MyMusicReceiver"
            android:enabled="true"
            android:exported="true" />

        <activity
            android:name=".MusicActivity"
            android:exported="false" />

        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true" />

        <activity
            android:name=".ClockActivity"
            android:exported="false" />

        <receiver
            android:name=".receiver.MyReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MY_BROADCAST" />

                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </receiver>

        <activity
            android:name=".SettingsActivity"
            android:exported="false"
            android:label="@string/title_activity_settings" />
        <activity
            android:name=".MainActivity"
            android:exported="false" />
        <activity
            android:name=".Splash"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        /ContentProvider声明
<provider
  android:name="androidx.core.content.FileProvider"
  android:authorities="${PACKAGE_NAME}.fileprovider"
  android:exported="false"
  android:grantUriPermissions="true">
  <meta-data
      android:name="android.support.FILE_PROVIDER_PATHS"
      android:resource="@xml/sharesfilepaths" />
</provider>
 </application>

</manifest>

在应用程序安装时,应用安装程序通过PackageInstaller服务解析应用安装包,并将AndroidManifest.xml中声明的四大组件信息保存到PackageManagerService中。

public class PackageManagerService extends IPackageManager.Stub
        implements PackageSender {
    
    ...
    
    //组件解析器,存储了系统所有应用程序的四大组件信息
    private final ComponentResolver mComponentResolver;
    
}

public class ComponentResolver {
    /** All available activities, for your resolving pleasure. */
    @GuardedBy("mLock")
    private final ActivityIntentResolver mActivities = new ActivityIntentResolver();

    /** All available providers, for your resolving pleasure. */
    @GuardedBy("mLock")
    private final ProviderIntentResolver mProviders = new ProviderIntentResolver();

    /** All available receivers, for your resolving pleasure. */
    @GuardedBy("mLock")
    private final ActivityIntentResolver mReceivers = new ActivityIntentResolver();

    /** All available services, for your resolving pleasure. */
    @GuardedBy("mLock")
    private final ServiceIntentResolver mServices = new ServiceIntentResolver();
    
    ...
}

context类

另外一个重要的类是Context
Context是关于应用程序环境的全局信息接口,Context是一个抽象类,它的实现都是由系统类实现的。Context允许访问应用程序特定的资源和类,如

资源管理器AssetsManager、
包管理器PackageManager、
文本图片主题资源Resource、
主线程消息循环Looper
四大组件操作,如启动Activity、BroadcastReceiver,接收Intent
SharedPreferences操作
私有目录文件操作
数据库创建、删除操作
获取系统服务
权限操作

在这里插入图片描述
如图,四大组件中的Activity和Service都是Context下的子类,Context

//android.content.Context

public abstract class Context {
    public abstract AssetManager getAssets();
    
    public abstract Resources getResources();
    
    public abstract PackageManager getPackageManager();

    public abstract ContentResolver getContentResolver();
    
    public abstract Looper getMainLooper();
    
    public final CharSequence getText(@StringRes int resId) {
        return getResources().getText(resId);
    }
    
    public final String getString(@StringRes int resId) {
        return getResources().getString(resId);
    }
    
    public abstract String getPackageName();
}

Context的实现类为ContextImpl,ContextImpl为Activity或其他应用程序组件提供了基础的Context实现。

//android.app.ContextImpl

class ContextImpl extends Context {

    //ContextImpl的构造方法是私有的,只能通过几个静态方法创建ContextImpl实例
    private ContextImpl(@Nullable ContextImpl container, 
                        @NonNull ActivityThread mainThread,
                        @NonNull LoadedApk packageInfo, 
                        @Nullable String splitName,
                        @Nullable IBinder activityToken, 
                        @Nullable UserHandle user, int flags,
                        @Nullable ClassLoader classLoader, 
                        @Nullable String overrideOpPackageName) {
        ...
    }

    ....

    //创建系统应用的上下文
    @UnsupportedAppUsage
    static ContextImpl createSystemContext(ActivityThread mainThread) {
        LoadedApk packageInfo = new LoadedApk(mainThread);
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0, null, null);
        context.setResources(packageInfo.getResources());
context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(), ontext.mResourcesManager.getDisplayMetrics());
        return context;
    }
    
    //基于系统应用Context创建的用于UI的系统上下文,此上下文具有可以主题化的资源
    static ContextImpl createSystemUiContext(ContextImpl systemContext, 
                                            int displayId) {
        final LoadedApk packageInfo = systemContext.mPackageInfo;
        ContextImpl context = new ContextImpl(null, systemContext.mMainThread, 
                                packageInfo, null,null, null, 0, null, null);
        context.setResources(createResources(null, packageInfo, null, displayId, 
                                null,packageInfo.getCompatibilityInfo()));
        context.updateDisplay(displayId);
        return context;
    }
    
    //创建普通应用级别的上下文,packageInfo指定了某个已安装的应用
    static ContextImpl createAppContext(ActivityThread mainThread, 
                                        LoadedApk packageInfo,
                                        String opPackageName) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo,        
                                                null, null, null, 0,
                                                 null, opPackageName);
        context.setResources(packageInfo.getResources());
        return context;
    }
    
    //创建Activity级别的上下文
    static ContextImpl createActivityContext(ActivityThread mainThread,
                                       LoadedApk packageInfo, 
                                       ActivityInfo activityInfo, 
                                       IBinder activityToken, //代表一个Activity
                                       int displayId,
                                       Configuration overrideConfiguration) {
        if (packageInfo == null) 
            throw new IllegalArgumentException("packageInfo");

        String[] splitDirs = packageInfo.getSplitResDirs();
        ClassLoader classLoader = packageInfo.getClassLoader();

        if (packageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "SplitDependencies");
            try {
           classLoader=packageInfo.getSplitClassLoader(activityInfo.splitName);
                splitDirs = packageInfo.getSplitPaths(activityInfo.splitName);
            } catch (NameNotFoundException e) {
        // Nothing above us can handle a NameNotFoundException, better crash.
                throw new RuntimeException(e);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            }
        }

        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, 
            activityInfo.splitName,activityToken, null, 0, classLoader, null);

        // Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY.
        displayId = (displayId != Display.INVALID_DISPLAY) ? displayId : 
                                                    Display.DEFAULT_DISPLAY;

        final CompatibilityInfo compatInfo = (displayId == 
                                            Display.DEFAULT_DISPLAY)
                                    ? packageInfo.getCompatibilityInfo()
                                : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;

        final ResourcesManager resourcesManager =ResourcesManager.getInstance();

        // Create the base resources for which all configuration contexts for this Activity
        // will be rebased upon.
        context.setResources(resourcesManager.createBaseActivityResources(
                activityToken,
                packageInfo.getResDir(),
                splitDirs,
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                classLoader));
        context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
                context.getResources());
        return context;
    }
    ...
}

除了ContextImpl类外,ContextWrapper类也继承了Context,但是ContextWrapper类并不自己实现了Context的方法,而是通过构造方法,代理给另外一个Context的实现。这样ContextWrapper的子类就可以在不修改ContextWrapper类的情况下,修改其调用方法的实现

public class ContextWrapper extends Context {
    Context mBase;
    
    public ContextWrapper(Context base) {
        mBase = base;
    }
    
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }
    
    ...
}

这里使用了代理模式,因为base实际上是Context类,真正最后调用的是ContextImpl的实现方法。

Activity、ContentProvider、Service三者的启动顺序

一个新的应用启动时,会优先初始化其Application类,创建了Application实例后,会立即调用其attach方法。
然后就会初始化应用中声明的ContentProvider:
ContentProvider初始化完成后,会再调用Application类的onCreate方法。

AMS在初始化完客户端的Application类后,会检查是否有需要运行的Service和BroadcastReceiver。
所以初始化顺序是Application.attach() > ContentProvider> Application.onCreate() > Activity/Service/BroadcastReceiver。

ContentProvider比Activity或Service初始化的顺序都要早,所以有些第三方库利用这个特性,通过ContentProvider自动初始化一些功能,而不用在Application中添加初始化代码。

Activity

生命周期

protected void onCreate(Bundle savedInstanceState);
Activity创建,用于初始化数据

protected void onStart();
Activity UI可见

protected void onRestart();
Activity UI从不可见变为可见

protected void onResume();
Activity UI可操作

protected void onPause();
Activity 暂停,UI可见,不可操作

protected void onStop();
Activity停止,UI不可见

protected void onDestroy();
Activity销毁

Service

Service是一种应用程序组件,用于两种使用场景:

后台执行长时间的任务,而不需要与用户交互。例如后台播放音乐
将本应用的功能通过接口的形式暴露给其他应用程序调用
两种启动Service的方式:
1、startService,Service会执行onCreate和onStartCommand。多次启动同一个Service不会多次执行onCreate,会多次执行onStartCommand
2、bindService,Service会执行onBind方法,返回给调用者一个Binder对象,用于功能接口调用。

Service优先级:
Service所在进程的优先级仅次于前台进程的优先级,系统会尽量避免杀死该进程,除非内存压力非常大。如果被系统杀死了,系统会稍后尝试重启该Service,并重新将Intent数据发送Service来恢复杀死之前的状态。

onStartCommand方法的返回值,决定Service被系统杀死后的操作

START_NOT_STICKY 被系统杀死后不会被重启
START_STICKY 被系统杀死后会重建,但是会发送一个null给onStartCommand
START_REDELIVER_INTENT 被系统杀死后会重建,并且会逐一发送等待处理的Intent给onStartCommand

startService调用

创建Service实例
调用Service的attach方法
调用Service的onCreate方法
以token为key保存到Map中,方便后续多次调用onStartCommand
告知AMS启动完成

bindService调用

bindService与startService不同的地方是,bindService需要提供一个ServiceConnection的回调接口,用来告知调用者已连接或断开连接。

Service启动时的生命周期

多次调用startService
执行一次Service的onCreate
多次执行Service的onStartCommand

多次调用bindService
执行一次Service的onCreate
执行一次Service的onBind
调用方的ServiceConnection也只会回调一次

content provider

ContentProvider用于在不同应用程序间提供数据,它在内部实现了跨进程的调用,不需要数据提供者或调用者关心。
调用者通过URI向ContentProvider查询数据。
一般是调用ContentResolver来获取数据

context.getContentResolver().query("查询的ContentProviderUri", "返回的列", "过滤行的条件", "过滤行的参数", "返回行数据的排序方式");


BroadcastReceiver

在Manifesh.xml文件中注册自定义的BroadcastReceiver,当意图过滤器中的动作发生时,会回调BroadcastReceiver中的onRecevie方法.
静态注册

       <receiver
            android:name=".receiver.MyReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MY_BROADCAST" />

                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </receiver>

动态注册

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);

registerReceiver(new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
      String action = intent == null ? "" : intent.getAction();
      if (Intent.ACTION_SCREEN_OFF.equalsIgnoreCase(action)) {
          Log.i(TAG, "onReceive: 屏幕关闭");
      }
      context.unregisterReceiver(this);
  }
}, intentFilter);

在Activity上下文中调用registerReceiver方法,动态注册一个广播接收者,同时指定了意图过滤器。

回到demo

介绍了一下四大组件,回头看一下demo
在这里插入图片描述

项目目录解释

  • main里面的java是整个app的入口,一定会有一个主活动,是你打开app后见到的第一个界面
  • res是自动生成的目录,里面用于存放各种资源文件,以及一些布局xml。例如:drawable里放一些背景图片资源。layout一般和activity对应,是当前活动的UI界面,里面会定义各种按钮以及文本框之类的组件。res里的控件都要注册一个id,这个是一种int类型的唯一标识。如图
  • 在这里插入图片描述
    这里注册了一个id/down,在代码中需要使用的时候就会用
    在这里插入图片描述

这种形式来获得这个按钮把它包装成一个对象

  • raw 中存储一些资源,音乐文件等
  • menu里是如图右上角菜单的一些组件的设置
  • 在这里插入图片描述
    下一个比较重要的文件是AndroidManifest.xml
    四大组件都要在里面进行注册,以及一些权限申请也要在里面写明
    gradle用于构建项目依赖一般也是自动生成
    音乐播放器功能主要由两个类实现
package com.example.note;

import androidx.appcompat.app.AppCompatActivity;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.widget.Button;
import android.widget.Toast;

public class MusicActivity extends AppCompatActivity {
    Button last, start, next;
    public static final String CTRL_ACTION = "com.example.note.CTRL";
    public static  final String UPDATE_ACTION="com.example.note.UPDATE";
    int status = 0x11;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_music);
        last = findViewById(R.id.up);
        start = findViewById(R.id.start);
        next = findViewById(R.id.down);
        Intent intent = new Intent(MusicActivity.this, MyService.class);
        ActivityReceiver activityReceiver = new ActivityReceiver();
        IntentFilter filter = new IntentFilter();
        filter.addAction(UPDATE_ACTION);
        registerReceiver(activityReceiver, filter);
        startService(intent);
        last.setOnClickListener(view -> {
            Intent intent1=new Intent(CTRL_ACTION);
            intent1.putExtra("control", 3);
            this.sendBroadcast(intent1);
        });
        start.setOnClickListener(view -> {
            Intent intent1=new Intent(CTRL_ACTION);
            intent1.putExtra("control", 1);
            sendBroadcast(intent1);
        });
        next.setOnClickListener(view -> {
            Intent intent1=new Intent(CTRL_ACTION);
            intent1.putExtra("control", 4);
            sendBroadcast(intent1);
        });

    }

    private class ActivityReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            //获取来自receive中intent的update消息,代表播放状态
            int update = intent.getIntExtra("update", -1);
            switch (update) {
                //播放歌曲
                case 0x11:
                    status = 0x11;
                    break;
                //播放>暂停
                case 0x12:

                    status = 0x12;
                    break;
            }

        }


    }
}



在onCreate方法里先启动MyService

    Intent intent = new Intent(MusicActivity.this, MyService.class);
     startService(intent);

分别绑定了三个按钮,并且每个按钮设置的监听事件是发送一个广播

        next.setOnClickListener(view -> {
            Intent intent1=new Intent(CTRL_ACTION);
            intent1.putExtra("control", 4);
            sendBroadcast(intent1);
        });

例如这个点击下一首的通过发送控制信息给Myservice的内部广播类来进行切歌的控制。

MyService类是另一个重要的实现类

package com.example.note;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.MediaPlayer;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.Nullable;

import com.example.note.receiver.MyReceiver;

import java.io.IOException;

public class MyService extends Service {
    @Nullable
    MyReceiver serviceReceiver;
    AssetManager am;
    String[] musics=new String[]{"music0.mp3","music1.mp3","music2.mp3","music3.mp3","music4.mp3"};
    MediaPlayer mPlayer;
    //0x11表示没有播放,0x12代表正在播放,0x13代表暂停
    int status=0x11;
    int current=0;
    public IBinder onBind(Intent intent) {
        return null;
    }
    public void onCreate(){
        super.onCreate();
        am=getAssets();
        //创建BroadcastReceiver
        serviceReceiver=new MyReceiver();
        //创建IntentFilter
        IntentFilter filter=new IntentFilter();
        filter.addAction(MusicActivity.CTRL_ACTION);
        registerReceiver(serviceReceiver,filter);
        mPlayer=new MediaPlayer();
        //为MediaPlayer播放完成事件绑定监听器
        mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                current++;
                if (current>=musics.length)
                {
                    current=0;
                }
                Intent sendIntent = new Intent(MusicActivity.UPDATE_ACTION);
                sendIntent.putExtra("current",current);
                //发送广播,将被Activity组件中的BroadcastReceiver接收
                sendBroadcast(sendIntent);
                //准备播放音乐
                prepareAndPlay(musics[current]);
            }
        });
    }
    private class MyReceiver extends BroadcastReceiver{

        @Override
        public void onReceive(Context context, Intent intent) {
            int control =intent.getIntExtra("control",-1);
            switch (control)
            {
                //播放或暂停
                case 1:
                    //原来处于没有播放状态
                    if (status==0x11)
                    {
                        //准备并播放音乐
                        prepareAndPlay(musics[current]);
                        status=0x12;
                    }
                    //原来处于播放状态
                    else if (status==0x12)
                    {
                        //暂停
                        mPlayer.pause();
                        //改变为暂停状态
                        status=0x13;
                    }
                    //原来处于暂停状态
                    else if (status==0x13)
                    {
                        //播放
                        mPlayer.start();
                        //改变状态
                        status=0x12;
                    }
                    break;
                //停止声音
                case 2:
                    //如果原来正在播放或暂停
                    if (status==0x12||status==0x13) {
                        //停止播放
                        mPlayer.stop();
                        status = 0x11;
                    }
                    break;
                case 3:
                    //原来处于没有播放或暂停状态
                    if (status==0x11||status==0x13)
                    {
                        if(current==0) {
                            current=4;
                            prepareAndPlay(musics[current]);
                        }
                        //准备并播放音乐
                        else {
                            current=current-1;
                            prepareAndPlay(musics[current]);
                        }
                        status=0x12;
                    }
                    //原来处于播放状态
                    else if (status==0x12)
                    {
                        //上一首//准备并播放音乐
                        if(current==0) {
                            current=4;
                            prepareAndPlay(musics[current]);
                        }
                        else {
                            current=current-1;
                            prepareAndPlay(musics[current]);
                        }
                    }
                    break;
                case 4:
                    //原来处于没有播放或暂停状态
                    if (status==0x11||status==0x13)
                    {
                        if(current==4) {
                            current=0;
                            prepareAndPlay(musics[current]);
                        }   //准备并播放音乐
                        else {
                            current=current+1;
                            prepareAndPlay(musics[current]);
                        }
                        status=0x12;
                    }
                    else if (status==0x12)
                    {
                        if(current==4) {
                            current=0;
                            prepareAndPlay(musics[current]);
                        }
                        else {
                            current=current+1;
                            prepareAndPlay(musics[current]);
                        }
                    }
                    break;
            }
            //广播通知Activity更改图标、文本框
            Intent sendIntent=new Intent(MusicActivity.UPDATE_ACTION);
            sendIntent.putExtra("update",status);
            sendIntent.putExtra("current",current);
            //发送广播,将被Activity组件中的BroadcastReceiver接收
            sendBroadcast(sendIntent);
        }
    }


    private void prepareAndPlay(String music)
    {
        try
        {
            //打开指定音乐文件
            AssetFileDescriptor afd=am.openFd(music);
            mPlayer.reset();
            //使用MediaPlayer加载指定的音乐文件
            mPlayer.setDataSource(afd.getFileDescriptor(),afd.getStartOffset(),afd.getLength());
            //准备声音
            mPlayer.prepare();
            //播放
            mPlayer.start();
            Toast.makeText(this,music+"正在播放",Toast.LENGTH_SHORT).show();
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这个类里面自己写的时候踩得雷基本上就是MediaPlayer的调用问题,对这个类的api了解不够导致总是出一些异常。姑且不谈
这里我们5首音乐全部放在assets里而非raw里,调用asserts需要通过AssetManage来调用
这个服务里也用到了广播组件,不过这里主要用来接收控制请求,和所有其他组件一样,都要进行注册,这里用了动态注册的方法

 IntentFilter filter=new IntentFilter();
        filter.addAction(MusicActivity.CTRL_ACTION);//在MusicActivity中定义的静态变量,也是这个广播被识别调用的重要标识,相当于一个身份证。
        registerReceiver(serviceReceiver,filter);

本demo中没有用到content provider,之后会考虑添加应用场景。

除了四大组件,我们可以看到还有一个很重要的类Intent类。
Intent用于启动Activity, Service, 以及BroadcastReceiver三种组件, 同时还是组件之间通信的重要媒介。
在demo里,可以看到,用广播传递控制信息,接收广播,开启服务,开启活动所用的都是Intent。
更多内容之后在研究吧。

总结

本demo中熟悉了一下三大组件的一些使用,但是代码没有很好的设计,看起来不够清晰。比如music
活动类还可以使用继承Onclicklistender的方式来重写onclick方法,对所有按钮的响应事件写进一个方法中。还有初版需求没有完成留下的没有用的界面,以后在写代码前要先思考整体的框架以及各个模块的功能,然后填充代码。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值