前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《移动互联网技术》第九章 感知与多媒体: 了解质感设计的基本原则和设计方法

《移动互联网技术》第九章 感知与多媒体: 了解质感设计的基本原则和设计方法

作者头像
猫头虎
发布2024-04-08 16:33:41
630
发布2024-04-08 16:33:41
举报

《移动互联网技术》课程简介

《移动互联网技术》课程是软件工程、电子信息等专业的专业课,主要介绍移动互联网系统及应用开发技术。课程内容主要包括移动互联网概述、无线网络技术、无线定位技术、Android应用开发和移动应用项目实践等五个部分。移动互联网概述主要介绍移动互联网的概况和发展,以及移动计算的特点。无线网络技术部分主要介绍移动通信网络(包括2G/3G/4G/5G技术)、无线传感器网络、Ad hoc网络、各种移动通信协议,以及移动IP技术。无线定位技术部分主要介绍无线定位的基本原理、定位方法、定位业务、数据采集等相关技术。Android应用开发部分主要介绍移动应用的开发环境、应用开发框架和各种功能组件以及常用的开发工具。移动应用项目实践部分主要介绍移动应用开发过程、移动应用客户端开发、以及应用开发实例。 课程的教学培养目标如下: 1.培养学生综合运用多门课程知识以解决工程领域问题的能力,能够理解各种移动通信方法,完成移动定位算法的设计。 2.培养学生移动应用编程能力,能够编写Andorid应用的主要功能模块,并掌握移动应用的开发流程。 3. 培养工程实践能力和创新能力。  通过本课程的学习应达到以下目的: 1.掌握移动互联网的基本概念和原理; 2.掌握移动应用系统的设计原则; 3.掌握Android应用软件的基本编程方法; 4.能正确使用常用的移动应用开发工具和测试工具。

第九章 感知与多媒体

本章小结:

1**、本单元学习目的**

通过学习如何使用移动设备的各种传感器和硬件设备来获取环境信息,掌握如何使用GPS实现定位功能,音视频播放功能,摄像头拍照功能;掌握界面设计原则、用户体验设计和质感设计。

2**、本单元学习要求**

(1) 掌握各种感知处理方法;

(2) 了解质感设计的基本原则和设计方法,并且通过不断的实践从复杂的事务中提炼出简洁、舒适的设计。

3**、本单元学习方法**

结合教材以及Android Studio开发软件,对传感器、摄像头、蓝牙等模块进行编程练习,运行调试,并在模拟器中观察运行情况。

4**、本单元重点难点分析**

重点

(1) 传感器

Android系统中包括两类传感器,分别是物理传感器和虚拟传感器。物理传感器可以直接采集各种物理特性,包括温度计、气压计、光传感器、心率计、加速度计、陀螺仪、指南针等等。虚拟传感器根据物理传感器采集的数据,通过融合算法计算出各种特性,比如:旋转矢量、重力、线性加速度等等。手机上的计步器也是一种虚拟传感器,它可以根据加速度计计算步数。另外,按照传感器的用途,可划分为:运动传感器、环境传感器和位置传感器。运动传感器测量加速度以及沿三个轴的旋转速度,包括加速度计,重力感应器,陀螺仪等等。环境传感器测量各种环境参数,例如:空气温度、照明等,包括气压计、光传感器、温度计等。位置传感器测量设备的物理位置,包括:GPS、方向传感器和磁力计等。

传感器的数据采集有不同的方式:第一、可以持续不断的采集数据,通常实时的连续获取数据常用于加速度计、陀螺仪等传感器;第二、在一段时间内,当传感器数据发生变化时采集数据,比如:心率计和计步器;第三、当传感器检测到某种特定事件时,开始采集数据,比如:红外传感器检测到人靠近时会触发相应的事件;第四、某些特定需求的数据采集。

Android通过SensorManager来管理传感器。首先创建SensorActivity,在onCreate函数中使用Context.getSystemService(String)来获取SensorManager。

public class SensorActivity extends AppCompatActivity { public static String TAG = “MainTAG”; private SensorManager sensorManager;

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sensor);

manager=(SensorManager) getSystemService(SENSOR_SERVICE); sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);

List sensorList; // 获取设备支持的所有传感器 sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL); List sensorNameList = new ArrayList();

for (Sensor sensor : sensorList) { Log.d(TAG, "传感器: " + sensor.getName()); }

}

}

如果要使用特定的传感器,需要从SensorManager中获取指定类型的传感器。使用SensorManager.getDefaultSensor(int type)得到指定的Sensor。type参数用来指定要获取的传感器类型,比如:

Sensor.TYPE_ORIENTATION:方向传感器;

Sensor.TYPE_ACCELEROMETER:重力传感器;

Sensor.TYPE_LIGHT:光线传感器;

Sensor.TYPE_MAGNETIC_FIELD:磁场传感器。

当外部环境发生变化时,Android系统首先通过传感器获取外部环境数据,然后将数据传递给监听器的监听回调函数。为了采集传感器数据,通过SensorManager为Sensor添加监听器。在使用完后,还要注销监听器。

sensorManager.registerListener(sensorListener,

sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),

SensorManager.SENSOR_DELAY_NORMAL);

第一个参数是监听器listener,第二个参数是传感器sensor,第三个参数是传感器的采样率rateUs,表示从传感器获取值的频率,它包括以下几个选项:

SensorManager.SENSOR_DELAY_FASTEST:最快,延迟最小;

SensorManager.SENSOR_DELAY_GAME:适合游戏的频率;

SensorManager.SENSOR_DELAY_NORMAL:正常频率;

SensorManager.SENSOR_DELAY_UI:最慢,适合界面UI变化的频率。

采样频率根据实际应用的需要来确定说。通常采用SENSOR_DELAY_NORMAL 或SENSOR_DELAY_GAME。如果采用更高的采样率,将耗费更多的资源,包括电量、CPU等。

接下来实现监听器。

private SensorEventListener sensorListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent sensorEvent) { if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { float x_lateral = sensorEvent.values[0]; float y_longitudinal = sensorEvent.values[1]; float z_vertical = sensorEvent.values[2];

代码语言:javascript
复制
   String info = "加速度传感器xyz轴的加速度分别为:\n" + x_lateral +
       "\n" + y_longitudinal + "\n" + z_vertical + "\n";

   sView.setText(info);
 }

}

@Override public void onAccuracyChanged(Sensor sensor, int accuracy) { }

};

当传感器采集的值发生变化时,触发调用函数onSensorChanged(SensorEvent event);当传感器精度发生变化时,触发调用函数onAccuracyChanged(Sensor sensor,int accuracy)。

关闭应用后,传感器的监听器不会自动释放资源,因此需要开发人员在适当的时候注销监听器。

@Override protected void onStop() { sensorManager.unregisterListener(sensorListener); super.onStop(); }

(2) GPS****定位和位置服务

下面利用移动设备的GPS芯片来定位经纬度坐标。在界面上用TextView控件显示定位的经纬度信息。

<TextView

android:id="@+id/location_text_view"

android:layout_width=“wrap_content”

android:layout_height=“wrap_content”

android:textSize=“28sp”/>

使用设备的定位功能需要授予权限,考虑一下是使用动态授权还是静态授权?静态权限如下:

注意:Andriod 6.0以后要使用动态权限。如果已经授权,就直接调用定位程序。

int checkPermission = ContextCompat.checkSelfPermission(LocationActivity.this,

Manifest.permission.ACCESS_FINE_LOCATION);

if (checkPermission != PackageManager.PERMISSION_GRANTED) {

ActivityCompat.requestPermissions(LocationActivity.this, new String[]{

Manifest.permission.ACCESS_FINE_LOCATION }, 1);

} else {

position();

}

通过getSystemService得到位置管理器对象。调用LocationManager的getProviders 函数获取所有可用的位置提供器,然后判断GPS是否打开,如果无法使用GPS,则看看是否能通过网络来定位。

locationManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);

List providerList = locationManager.getProviders(true);

if (providerList.contains(LocationManager.GPS_PROVIDER)) {

provider = LocationManager.GPS_PROVIDER;

} else if (providerList.contains(LocationManager.NETWORK_PROVIDER)) {

provider = locationManager.NETWORK_PROVIDER;

} else {

Toast.makeText(this, “无法提供位置信息”, Toast.LENGTH_SHORT).show();

return;

}

通过LocationManager的getLastKnownLocation函数获得最近的位置信息,同时在界面上更新当前的位置。虽然获取了当前的位置信息,但是用户可能会随时移动,怎样才能在位置改变的时候获取最新的位置信息呢?LocationManager 提供了请求定位更新函数requestLocationUpdates,它的第二个参数表示监听位置变化的时间间隔;第三个参数表示监听位置变化的距离间隔;第四个参数是位置监听器对象。

Location location = locationManager.getLastKnownLocation(provider);

if (location != null) {

​ updateLocation(location);

}

locationManager.requestLocationUpdates(provider, 1000, 1, locationListener);

其中,“1000”表示监听位置变化的时间间隔以毫秒为单位,“1”表示监听位置变化的距离间隔以米为单位。

位置更新代码是在界面上显示经纬度信息。

private void updateLocation(Location location) {

if (location == null) {

​ return;

}

String currentPosition = “GPS 定位:” + “\n”

​ + " 经度: " + location.getLongitude() + “\n”

​ + " 纬度: " + location.getLatitude();

locationTextView.setText(currentPosition);

}

创建位置监听器,监听位置的变化,一旦监听时间间隔和距离间隔发生改变就调用updateLocation函数,来更新位置。

LocationListener locationListener = new LocationListener() {

@Override

public void onStatusChanged(String provider, int status, Bundle extras) { }

@Override

public void onProviderEnabled(String provider) { }

@Override

public void onProviderDisabled(String provider) { }

@Override

public void onLocationChanged(Location location) {

​ updateLocation(location);

}

};

注意:在销毁函数中,要移除位置监听器。

@Override

protected void onDestroy() {

super.onDestroy();

if (locationManager != null) {

​ locationManager.removeUpdates(locationListener);

}

}

通过GPS确定经纬度以后,还需要结合电子地图才能知道自己当前所在的位置。很多电子地图软件提供了定位和导航功能,比如百度地图就提供了Android定位的SDK库。通常第三方的定位库还提供基站、WiFi、地磁、蓝牙、传感器等多种定位方式,适用于室内、室外等多种定位场景;并且它们都有出色的定位性能,具有定位精度高、覆盖范围广、定位流量小、定位速度快等特点。如果要使用第三方定位服务,还需要申请定位API Key,很多公司提供了定位API的接口说明,可以直接在网上查阅相关的资料。

在MapActivity中,放置多个控件显示当前位置的经度和纬度,可以选择手工定位和GPS定位,设置目标地以后,点击按钮可以实现路径规划功能,地图上是一个切换按钮可以切换显示普通地图和卫星地图。在应用中,使用高德地图实现位置服务功能。

首先要去高德开放平台注册成为开发者(http://lbs.amap.com/), 注册成为高德开发者需要分三步:第一步,注册高德开发者;第二步,去控制台创建应用;第三步,获取使用API函数的Key。

MapActivity实现OnClickListener监听器,用来处理按钮的点击事件,OnGeocodeSearchListener是地理编码搜索监听器,OnRouteSearchListener是路由搜索监听器,它们用来定位和路径规划;接下来,定义位置管理器等多个对象。

public class MapActivity extends AppCompatActivity implements View.OnClickListener, OnGeocodeSearchListener, OnRouteSearchListener {

代码语言:javascript
复制
   private MapView mapView;
   private AMap aMap;
   private LocationManager locationManager;

   *//* *导航**

* private Button btnNavigation; private GeocodeSearch geocodeSearch; private RouteSearch routeSearch; private EditText editTextAddress; private String province = “四川”;

@Override protected void onCreate(Bundle savedInstanceState) {

​ … … ​ mapView = findViewById(R.id.map); ​ // *必须回调MapView的**onCreate()方法 * mapView.onCreate(savedInstanceState); ​ init();

​ ToggleButton tb = findViewById(R.id.tb); ​ tb.setOnCheckedChangeListener(

new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView,

​ boolean isChecked) { ​ if (isChecked) { * * aMap.setMapType(AMap.MAP_TYPE_SATELLITE); ​ } else { ​ aMap.setMapType(AMap.MAP_TYPE_NORMAL);

} } });

onCreate函数,获取定位管理器,为GPS单选按钮设置监听器,如果RadioButton选择GPS定位,则通过监听器监听GPS提供的定位信息的改变。requestLocationUpdates函数的第一个参数是定位方式,第二个参数是定位更新的最小时间间隔(毫秒),第三个参数是定位更新的最小距离(米),第四个参数是定位监听器,接下来实现监听代码。

locationManager = (LocationManager) getSystemService( Context.LOCATION_SERVICE);

RadioButton rb = findViewById(R.id.radio_btn_gps);* * rb.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView , boolean isChecked) { * * if (isChecked) { * * locationManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, 300, 8, new LocationListener() {

@Override public void onLocationChanged(Location loc) { * * updatePosition(loc); }

代码语言:javascript
复制
@Override
public void onStatusChanged(String provider, int status, Bundle extras) { }

@Override
public void onProviderEnabled(String provider) {*

* updatePosition(locationManager.getLastKnownLocation(provider)); }

代码语言:javascript
复制
@Override
public void onProviderDisabled(String provider) { }

});

当位置改变时,将触发onLocationChanged函数,调用updatePosition函数,根据GPS提供的定位信息来更新位置

(3) 视频播放

在Android系统中,有三种实现视频播放的方式:(1)使用系统自带的播放器,并且将intent的action指定为ACTION_VIEW,Data指定为Uri,Type指定为媒体的MIME类型。(2)使用VideoView控件来播放视频。在布局文件中设置VideoView控件,然后编写视频播放控制函数来控制播放。(3)使用系统的MediaPlayer类和SurfaceView控件来播放视频。

下面用VideoView控件来实现一个简易的视频播放器。首先,创建视频播放界面的布局文件,视频播放要用到VideoView控件。注意:读写文件要申请授权。

<VideoView

android:id="@+id/video_view"

android:layout_width=“match_parent”

android:layout_height=“wrap_content” />

<LinearLayout

<Button

​ android:text=“播放”

​ android:onClick=“play”/>

可以在SD卡的根目录下存放要播放的视频文件。因为视频文件存放在SD卡上,在MediaActivity中,要用getExternalStorageDirectory获取外部存储目录。注意设置访问权限:

如果需要设置动态权限,需要在MediaActivity中编写运行时权限检查代码:

int checkPermission = ContextCompat.checkSelfPermission(MediaActivity.this,

​ Manifest.permission.WRITE_EXTERNAL_STORAGE);

if (checkPermission != PackageManager.PERMISSION_GRANTED) {

ActivityCompat.requestPermissions(MediaActivity.this, new String[]{

​ Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1);

} else {

// 通过视频文件名(绝对路径)创建一个文件

File file = new File(Environment.getExternalStorageDirectory(), “Androidstudio.3gp”);

}

videoView.setVideoPath(file.getPath());

播放功能很简单,在三个函数中分别调用控件的开始、暂停和恢复功能。

public void play(View view) {

if (!videoView.isPlaying()) {

​ videoView.start();

}

}

public void pause(View view) {

if (videoView.isPlaying()) {

​ videoView.pause();

}

}

public void resume(View view) {

if (videoView.isPlaying()) {

​ videoView.resume();

}

}

难点

(1) 摄像头拍照

Android智能手机都会提供照相功能,大部分手机的摄像头都会支持光学变焦、曝光以及快门等功能。下面通过摄像头实现拍照功能,并将拍摄的相片显示在界面上。首先定义拍照方法,在启动拍照之前先判断内存是否可用;然后通过重写onActivityResult()方法,获取拍好的图片。

创建一个拍照的界面。包括一个按钮和一个图片视图。

<LinearLayout

<Button

​ android:id="@+id/take_picture"

​ android:text=“拍照”

​ android:textSize=“28sp”/>

<ImageView

​ android:id="@+id/picture"

/>

在拍照之前,先创建照片的存储文件。在缓存目录中存储。

takePicture.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

​ File savePicture = new File(getExternalCacheDir(), “MyPicture.jpg”);

​ try {

​ if (savePicture.exists()) {

​ savePicture.delete();

​ }

​ savePicture.createNewFile();

​ } catch (IOException e) {

​ e.printStackTrace();

​ }

}

如果SDK的版本是Android 7.0以上,调用FileProvider的getUriForFile 函数将File对象转换成一个封装的Uri 对象。FileProvider的第二个参数是一个唯一性字符串,第三个参数是刚刚创建的用来存储照片的文件对象。

如果版本低于Android 7.0,调用Uri的fromFile 函数将直接将文件对象转成Uri对象,Uri指示照片的本地路径。然后用Intent启动摄像头,拍照的action为:android.media.action.IMAGE_CAPTURE。把拍照后的输出地址也存入Intent,然后打开拍摄界面。用户在拍完照片后,会把照片输出到指定的MyPicture.jpg中。

if (Build.VERSION.SDK_INT >= 24) {

picUri = FileProvider.getUriForFile(CameraActivity.this,

​ “pers.cnzdy.tutorial.fileprovider”, savePicture);

} else {

picUri = Uri.fromFile(savePicture);

}

Intent Intent = new Intent(“android.media.action.IMAGE_CAPTURE”);

Intent.putExtra(MediaStore.EXTRA_OUTPUT, picUri);

startActivityForResult(Intent, TAKE_PICTURE);

拍照完成以后,结果将返回onActivityResult函数。如果成功(resultCode = RESULT_OK),就解析出图片,显示在界面上。

@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case TAKE_PICTURE: if (resultCode == RESULT_OK) { try { Bitmap bitmap = BitmapFactory.decodeStream( getContentResolver().openInputStream(picUri)); picture.setImageBitmap(bitmap); } catch (FileNotFoundException e) { e.printStackTrace(); } } break; default: break; } }

FileProvider是 Android 7.0 新增的一个类,继承自ContentProvider,因此需要在配置文件AndroidManifest中进行注册。注意android:authorities的属性值和调用FileProvider的getUriForFile函数的第二个参数要一致。

<provider

android:name=“android.support.v4.content.FileProvider”

android:authorities=“pers.cnzdy.tutorial.fileprovider”

android:exported=“false”

android:grantUriPermissions=“true”>

<meta-data

​ android:name=“android.support.FILE_PROVIDER_PATHS”

​ android:resource="@xml/file_provider_paths" />

用来指定Uri的共享路径,file_provider_paths是一个xml文件,需要创建。在res目录下,创建一个xml目录,然后在目录上点击鼠标右键,选择New—File,创建file_provider_paths.xml文件,xml文件的内容如下:

<?xml version="1.0" encoding="utf-8"?>

​ ](http://schemas.android.com/apk/res/android)

file_provider_paths.xml文件的标签用来指定Uri共享,name属性设定为“my_pictures”,path属性表示共享的路径位置,设置空值就表示将整个SD卡进行共享,也可以设置为只共享存放MyPicture.jpg这张照片的路径。

(2) 音乐播放器

在Android系统中,提供了多种播放音频的方式,包括:SoundPool、MediaPlayer、AudioTrack、Ringtone等等。

SoundPool用于管理和播放应用程序的音频资源,主要用于播放时间短,延迟小的声音。它支持多个音频文件同时播放,占用的资源较少,适合播放按键音、消息提示音等短促音效的场景。

MediaPlayer是Android内置的多媒体播放类,在android.media.MediaPlayer包中,它包含了音频和视频播放功能。MediaPlayer适用于播放时间较长,延迟要求不高,能全面控制和操作播放过程的情况。MediaPlayer能播放多种格式的声音文件,比如MP3、AAC、WAV、OGG、MIDI等等。

AudioTrack实现PCM(Pulse Code Modulation)音频流的回放,是更底层的音频播放方式。AudioTrack支持流式播放,可读取本地和网络音频流。相比于MediaPlayer,它更加高效,适用于实时播放音频的场景,如加密音频播放。AudioTrack只能播放已经解码的PCM流,如果要播放其它格式的音频文件,需要相应的解码器。

AsyncPlayer对MediaPlayer进行封装,提供了异步音频播放功能。由于播放等操作都在新线程中执行,不会阻塞UI线程。AsyncPlayer适用于异步播放,不需要复杂控制。

Ringtone提供铃声、提示音等系统类声音的播放功能。另外,RingtoneManager管理铃声数据库,包括:来电铃声(TYPE_RINGTONE)、提示音(TYPE_NOTIFICATION)、闹钟铃声(TYPE_ALARM)等。通常Ringtone类和RingtoneManager类在一起使用。

下面构造一个音乐播放器,实现音乐播放、上一曲、下一曲、开始/暂停、拖动进度条实现快进和快退等功能。

布局文件activity_music_player.xml采用LinearLayer布局,加入ListView,随后再加入进度条控件SeekBar。

<SeekBar

​ android:id="@+id/sb"

​ android:layout_width=“match_parent”

​ android:layout_height=“40dp”

​ android:maxHeight=“4dp”

​ android:minHeight=“4dp”

​ android:paddingBottom=“4dp”

​ android:paddingLeft=“14dp”

​ android:max=“240”

​ android:paddingRight=“14dp”

​ android:paddingTop=“4dp” />

接下来添加四个按钮:上一首、开始播放、暂停和下一首。

<Button

android:id="@+id/btn_last"

android:layout_width=“wrap_content”

android:layout_height=“wrap_content”

android:text=“下一首”/>

… … … …

在MusicPlayerActivity类中声明对应的变量,并完成初始化。

public class MusicPlayerActivity extends AppCompatActivity implements Runnable {

boolean firstplay = true;

private Button btnStart, btnStop, btnNext, btnLast;

private ListView listView;

// 显示当前播放音乐的名称、播放的时间、以及歌曲长度

private TextView music_Info;

private SeekBar seekBar;

private MusicPlayerService musicService = new MusicPlayerService();

​ @Override

protected void onCreate(Bundle savedInstanceState) {

​ super.onCreate(savedInstanceState);

​ setContentView(R.layout.activity_music_player);

btnStart = (Button) findViewById(R.id.btn_star);

​ btnStart.setOnClickListener(new View.OnClickListener() {

​ @Override

​ public void onClick(View view) {

​ try {

​ if (firstplay) {

​ musicService.play();

​ firstplay = false;

​ } else {

​ if (!musicService.player.isPlaying()) {

​ musicService.goPlay();

​ } else if (musicService.player.isPlaying()) {

​ musicService.pause();

​ }

​ }

​ } catch (Exception e) {

​ Log.i(“TAG”, “音乐播放异常!”);

​ }

​ }

​ });

btnStop = (Button) findViewById(R.id.btn_stop);

​ btnStop.setOnClickListener(new View.OnClickListener() {

​ @Override

​ public void onClick(View view) {

​ try {

​ musicService.stop();

​ firstplay = true;

​ seekBar.setProgress(0);

​ txtInfo.setText(“暂停已经停止”);

​ } catch (Exception e) {

​ Log.i(“LAT”, “暂停异常!”);

​ }

​ }

​ });

​ … …

}

}

“上一首”和“下一首”功能的代码类似,都是调用MusicService中的对应函数,具体实现可自行补全。接下来实现进度条功能:

(1)当拖动进度条时,从拖动位置开始播放音乐;

(2)根据音乐的播放进度显示当前已播放时间。

music_Info = (TextView) findViewById(R.id.textView_music_info);

seekBar = (SeekBar) findViewById(R.id.sb);

seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {

@Override

// SeekBar进度值发送改变

public void onProgressChanged(SeekBar seekBar, int i, boolean b) { }

@Override

// 开始拖动SeekBar

public void onStartTrackingTouch(SeekBar seekBar) { }

@Override

// SeekBar停止拖动

public void onStopTrackingTouch(SeekBar seekBar) {

​ int progress = seekBar.getProgress();

​ int musicMax = musicService.player.getDuration(); //歌曲播放时间

​ int seekBarMax = seekBar.getMax();

​ // 从停止处开始播放音乐

​ musicService.player.seekTo(musicMax * progress / seekBarMax);

}

});

MusicPlayerService类还要实现播放、恢复播放、获取当前进度、上一首、下一首、暂停和停止功能。

public void play() { try { player.reset(); String dataSource = musicList.get(musicId); // 获取当前播放音乐的路径 setPlayName(dataSource); // 设置 musicName

代码语言:javascript
复制
 player.setAudioStreamType(AudioManager.STREAM_MUSIC);
 player.setDataSource(dataSource); // 设置播放路径
 player.prepare();
 player.start();

 player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
   public void onCompletion(MediaPlayer arg0) {
     next(); // 歌曲播放完毕,自动播放下一首
   }
 });

} catch (Exception e) { Log.v(“TAG”, e.getMessage()); } } // 继续播放 public void resume(){ int position = getCurrentProgress(); player.seekTo(position); // 恢复当前播放位置 try { player.prepare(); } catch (Exception e) { e.printStackTrace(); } player.start(); }

public int getCurrentProgress() { if (player != null & player.isPlaying()) { return player.getCurrentPosition(); } else if (player != null & (!player.isPlaying())) { return player.getCurrentPosition(); } return 0; }

public void next() { musicId = musicId == musicList.size() - 1 ? 0 : musicId + 1; play(); }

public void last() { musicId = musicId == 0 ? musicList.size() - 1 : musicId - 1; play(); }

// 暂停播放 public void pause() { if (player != null && player.isPlaying()){ player.pause(); } }

public void stop() { if (player != null && player.isPlaying()) { player.stop(); player.reset(); } }

在AndroidManifest文件中配置服务。

(3)质感界面设计

“置于用户控制之下”、“保持界面的一致性”和“减轻用户的记忆负担”,通常称之为界面设计的“黄金三原则”。置于用户控制之下要求不强迫用户完成操作步骤,允许交互的中断和撤消。界面要保持清晰一致便于用户理解和使用。另外,由于人的短期记忆非常不稳定,因此对用户来说浏览比记忆更容易。

现在手机已经不仅仅是一个通话设备,它能够感知环境,提供各种智能化的服务。移动设备能够持续收集来自GPS、摄像头、麦克风和其它传感器的数据,并且通过这些数据感知环境的变化,然后作出反应,比如手机上的GPS、陀螺仪、气压计、麦克风,能跟踪用户的位置、方向,了解用户的各种信息,从而识别当前用户的状态。

2014年6月25日在Google I/O大会上宣布了新的Android界面设计——Material Design(质感设计)。Material Design要求交互和界面视觉更符合现实世界的物理反馈法则,比如一个小球下落,在真实世界中是一个加速的过程,如果在Android界面上显示小球下落的动画,也要有类似现实世界的感觉。质感设计关心界面上实体的光效、表面质感、运动感、实体感、层次、深度、与其他物体的叠放逻辑、动态效果、以及空间合理化利用等等。质感设计就像把交互界面变成了一张张的卡片。利用质感设计的API 接口,可以用来设计自己的具有Material Design的交互界面。

在界面上,菜单选项不显示在主屏幕上,而是通过滑动的方式将隐藏的菜单显示出来。滑动菜单只在需要的时候显示,节省了屏幕空间。实现滑动菜单需要用到DrawerLayout布局。DrawerLayout分为侧边菜单和主内容区两部分,侧边菜单提供滑动的展开与隐藏功能;主内容区用来设置菜单项,比如用ListView显示菜单项,它由开发者实现。

创建MaterialDesignActivity,在它的布局文件activity_material_design.xml中使用DrawerLayout布局。

代码语言:javascript
复制
<?xml version="1.0" encoding="utf-8"?>
 <android.support.v4.widget.DrawerLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:id="@+id/slide_menu_drawer_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent" >
 
   <FrameLayout
   android:layout_width="match_parent"
   android:layout_height="match_parent" >
     <android.support.v7.widget.Toolbar
       android:id="@+id/toolbar"
       android:layout_width="match_parent"
       android:layout_height="?attr/actionBarSize"
       android:background="?attr/colorPrimary"
       android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
       app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
     </android.support.v7.widget.Toolbar>
   </FrameLayout>  
代码语言:javascript
复制
<ImageView
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:layout_centerInParent="true"
   android:layout_gravity="start"
   android:src="@drawable/slide_bird"
   android:background="#FFF"/>


 </android.support.v4.widget.DrawerLayout>

在DrawerLayout中放置两个控件。第一个控件是Toolbar,它放在FrameLayout布局中,作为主屏幕中显示的内容(主内容区)。第二个控件放置一个ImageView控件,作为滑动菜单(侧边菜单)显示的内容,当然也可以使用其他控件。注意:主内容区的布局代码要放在侧滑菜单布局代码的前面,以便DrawerLayout能够判断哪个控件是侧滑菜单,哪个控件是主内容区。

在设置侧边菜单时,要注意设置控件的layout_gravity属性,也就是必须告诉DrawerLayout滑动菜单是在屏幕的左边还是右边,指定left表示在左边,指定right表示在右边,如果指定了start,表示根据系统语言自动判断。英语、汉语等从左到右显示的语言,滑动菜单在左边;阿拉伯语等从右到左的语言,滑动菜单就在右边。

DrawerLayout侧边菜单的展开与隐藏事件通过DrawerLayout.DrawerListener来监听。当触发菜单展开与隐藏事件,可以更新Toolbar菜单或进行其他操作。

下面在MaterialDesignActivity中添加代码实现滑动菜单:

public class MaterialDesignActivity extends AppCompatActivity { private DrawerLayout slideMenuDrawerLayout;

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_material_design);

代码语言:javascript
复制
 Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
 setSupportActionBar(toolbar);

 slideMenuDrawerLayout = (DrawerLayout)

findViewById(R.id.slide_menu_drawer_layout); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) {

// 显示actionBar上的导航按钮 actionBar.setDisplayHomeAsUpEnabled(true);

// 在actionBar上设置导航按钮图标 actionBar.setHomeAsUpIndicator(R.drawable.ic_menu); } }

public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.toolbar, menu); return true; }

@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: slideMenuDrawerLayout.openDrawer(GravityCompat.START); case R.id.search: Toast.makeText(this, “搜索”, Toast.LENGTH_SHORT).show(); break; case R.id.delete: Toast.makeText(this, “删除”, Toast.LENGTH_SHORT).show(); break; case R.id.add: Toast.makeText(this, “增加”, Toast.LENGTH_SHORT).show(); default: } return true; } }

通常情况下滑动菜单隐藏起来,为了让用户知道应用软件有滑动菜单功能,需要提供一个提示让用户知道。在界面上,通过ActionBar的导航按钮来提示用户。ActionBar由Toolbar实现。

通过以上代码实现了滑动菜单功能。作为Material Design的一种设计,滑动菜单为移动应用的开发者提供了很好的设计理念。只要遵循Material Design的各种规范和建议来构造应用系统,最终将创建统一、美观的应用界面。

(4) 蓝牙

Android系统中使用蓝牙设备的基本工作流程,首先,要申请蓝牙设备的使用权限,获得BluetoothAdapter对象,判断当前设备中是否拥有蓝牙设备;判断当前设备中的蓝牙设备是否已经打开;通过扫描获取周围的蓝牙设备对象。

BluetoothActivity实现了多个接口包括:视图监听器,AdapterView,CompoundButton改变状态按钮的监听器,checkBox控件用来开启和关闭蓝牙设备的事件监听器,蓝牙连接监听器,蓝牙接收监听器。BluetoothAdapter类可以对蓝牙进行基本操作,比如:启动设备发现(startDiscovery), 获取已配对设备(getBoundedDevices), 通过mac蓝牙地址获取蓝牙设备(getRemoteDevice), 从其它设备创建一个监听连接等等。最后,蓝牙列表对象用来保存扫描到的蓝牙设备。

public class BluetoothActivity extends AppCompatActivity implements View.OnClickListener, AdapterView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, BluetoothConnectTask.BlueConnectListener, InputDialogFragment.InputCallbacks, BluetoothAcceptTask.BlueAcceptListener { private static final String TAG = “BluetoothActivity”;

private CheckBox checkBox; private TextView textView; private ListView listView; private BluetoothAdapter bluetoothAdapter; private ArrayList bluetoothList =

​ new ArrayList();

在onCreate函数中,首先,调用自定义BluetoothTool类的getBlueToothStatus函数判断蓝牙设备是否开启,同时设置checkbox的状态,接着,调用蓝牙适配器的getDefaultAdapter函数获取本机蓝牙设备。

@Override protected void onCreate(Bundle savedInstanceState) { … … if (BluetoothTool.getBlueToothStatus(this) == true) { checkBox.setChecked(true); }

checkBox.setOnCheckedChangeListener(this); textView.setOnClickListener(this);

bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter == null) { Toast.makeText(this, “本机未找到蓝牙功能”,

​ Toast.LENGTH_SHORT).show(); ​ finish(); } }

实现checkBox的onCheckedChanged方法,如果checkbox开启,则调用beginDiscovery函数开始扫描蓝牙设备,并通过intent启动蓝牙设备设置界面,修改蓝牙设备的可见性,Intent的动作ACTION_REQUEST_DISCOVERABLE 表示请求用户选择是否使该蓝牙设备能被发现(扫描);如果checkbox关闭,则取消扫描,并且设置蓝牙的状态,清理蓝牙列表,同时清理界面上ListView控件显示的蓝牙设备。

@Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (buttonView.getId() == R.id.check_box_bluetooth) { if (isChecked == true) { beginDiscovery(); Intent intent = new Intent(

BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); startActivityForResult(intent, 1);* * } else { cancelDiscovery(); BluetoothTool.setBlueToothStatus(this, false); bluetoothList.clear(); BluetoothListAdapter adapter = new BluetoothListAdapter(this, bluetoothList); listView.setAdapter(adapter); } } }

发现(扫描)蓝牙函数,首先清理蓝牙列表,通过蓝牙列表适配器将蓝牙设备列表与ListView控件关联起来,然后,调用bluetoothAdapter的startDiscovery开始扫描设备。取消发现(扫描)函数,从handler中取消刷新回调,并通过bluetoothAdapter对象取消发现操作。

private void beginDiscovery() { if (bluetoothAdapter.isDiscovering() != true) { bluetoothList.clear(); BluetoothListAdapter adapter = new BluetoothListAdapter( BluetoothActivity.this, bluetoothList); listView.setAdapter(adapter); textView.setText(“正在搜索蓝牙设备”); bluetoothAdapter.startDiscovery(); } }

private void cancelDiscovery() { handler.removeCallbacks(refresh); textView.setText(“取消搜索蓝牙设备”); if (bluetoothAdapter.isDiscovering() == true) { bluetoothAdapter.cancelDiscovery(); } }

本章习题:

1**、本单元考核点**

各种传感器的基本概念和使用方法。

Android系统中GPS的定位方法。

Android系统音视频播放的使用方法。

使用摄像头实现拍照功能。

界面设计原则、用户体验设计和质感设计(Material Design)。

2**、本单元课后习题**

1、说明SoundPool与MediaPlayer的区别,以及在什么情况下使用SoundPool。 答案:在Android开发中经常使用MediaPlayer来播放音频文件,但是MediaPlayer存在一些不足:资源占用量较高、延迟时间较长、不支持多个音频同时播放等。这些缺点决定了MediaPlayer在某些场合的使用情况不会很理想,例如在对时间精准度要求相对较高的游戏开发中。在游戏开发中,经常需要播放一些游戏音效(比如:子弹爆炸,物体撞击等),这些音效的共同特点是短促、密集、延迟程度小。在这样的场景下,可以使用SoundPool代替MediaPlayer来播放这些音效。 MediaPlayer:占用资源较高,不支持同时播放多个音频。 SoundPool:可以同时播放多个短促的音频,而且占用的资源较少。适合在程序中播放按键音,或者消息提示音等。 3、。什么是ANR,如何避免它? 答案:ANR(Application Not Responding)是指程序不响应,在用户使用过程中,应用程序有一段时间响应不够灵敏,系统会向用户显示一个对话框,这个对话框称作应用程序无响应的对话框。 避免ANR: Android应用程序通常运行在一个单独的线程里面,称谓主线程,所以在主线程里面少做一些耗时长的程序,而是利用子线程来操作一些繁琐的事情,用Handler来把子线程处理的消息返回给主线程。

参考资源:

1、GitHub:https://github.com

2、刘望舒著.Android进阶之光.北京:电子工业出版社,2017.

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-07-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 《移动互联网技术》课程简介
  • 第九章 感知与多媒体
    • 本章小结:
      • 1**、本单元学习目的**
      • 2**、本单元学习要求**
      • 3**、本单元学习方法**
      • 4**、本单元重点难点分析**
    • 重点
      • (1) 传感器
      • (2) GPS****定位和位置服务
      • (3) 视频播放
    • 难点
      • (1) 摄像头拍照
      • (2) 音乐播放器
      • (3)质感界面设计
    • 本章习题:
      • 参考资源:
      相关产品与服务
      云开发 CloudBase
      云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档