三、翻页类视图
1.翻页视图
(1)作用和外观
翻页视图ViewPager允许用户在屏幕上左右滑动来切换显示的页面项,相当于一种横向显示的列表,它的默认外观是一片空白区域,ViewPager装载页面项之后如图:
显然,为了降低资源使用,Android只需要预先生成三个页面项(前一个,当前,后一个)就可以了,用户
左右滑动会预览前后的页面项,当滑动一定的距离之后才能切换页面项并触发监听器的回调方法。
(2)使用方法
翻页视图同样使用适配器来生成页面项,使用监听器来监听页面切换事件。
它的常用方法有:
①void setAdapter(PagerAdapter adapter):设置适配器adapter,它必须是PagerAdapter的子类。
②void setCurrentItem(int item):选择翻页视图当前显示的页面项。
③void addOnPageChangeListener(ViewPager.OnPageChangeListener listener):设置翻页视图的页面切换事件的监听器,监听器listener需要实现三个方法:
abstract void onPageScrollStateChanged(int state)
abstract void onPageScrolled(int position, float positionOffset, int positionOffsetPixels)
abstract void onPageSelected(int position):分别在翻页视图滑动状态发生改变,滑动距离改变(可在此获取用户手指具体的滑动距离)和滑动结束时回调。
翻页适配器PagerAdapter和BaseAdapter的用法类似:继承PagerAdapter的子类需要在构造函数中传入页面项的显示数据,在getCount方法中得到页面项的数目,在instantiateItem方法中生成具体的单个页面项,另外,PagerAdapter还可以使用destroyItem方法来回收页面项资源,使用getPageTitle方法获取指定页面项的标题文字。
翻页标题栏PagerTitleStrip和PagerTabStrip都通常在布局文件中作为ViewPager的子控件,都可以在翻页视图的上方显示页面项的标题。其中,PagerTitleStrip只有单纯的文本显示功能,无法通过点击标题项进行翻页视图页面的切换,而PagerTabStrip的文本下方有横线的标题项代表当前页面项,点击标题项即可同时切换页面项。通常,由于翻页标题栏这个控件内部自动和绑定的翻页视图进行交互,所以开发者不需要对翻页标题栏进行逻辑控制,就把当做一个文本显示控件,常见的处理是在布局文件或代码中里设置一下它的文本显示的属性。注意,布局文件中翻页视图ViewPager和翻页标题栏的标签名都要要填全路径类名,如androidx.viewpager.widget.ViewPager
这样,不过,我们也不会傻乎乎的手打代码,一般输入类名的关键字母,AS会弹出一个备选框帮我们自动补全代码的。
接下来通过一个实例来熟悉一下翻页视图的使用方法:
页面布局如图:
首先,继承翻页适配器PagerAdapter,在子类中中生成具体的页面项:
public class ImagePagerAdapter extends PagerAdapter {
private Context mContext; // 一个上下文对象
private ArrayList<ImageView> mViewList = new ArrayList<>();// 图像视图队列
private ArrayList<SoftwareBean> mSoftwareList;// 工具软件队列
// 图像翻页适配器的构造函数,传入上下文与工具软件队列
public ImagePagerAdapter(Context context, ArrayList<SoftwareBean> softwareList) {
mContext = context;
mSoftwareList = softwareList;
// 给每个工具软件分配一个专用的图像视图
for (int i = 0; i < mSoftwareList.size(); i++) {
ImageView view = new ImageView(mContext);
view.setLayoutParams(new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
view.setImageResource(mSoftwareList.get(i).image);
view.setScaleType(ScaleType.FIT_CENTER);
// 把一个装配完成的图像视图添加到队列中
mViewList.add(view);
}
}
// 获取页面项的个数
public int getCount() {
return mViewList.size();
}
//页面项是否和某个对象关联
@Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0 == arg1;
}
// 从容器中销毁指定位置的页面
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView(mViewList.get(position));
}
// 实例化指定位置的页面项,并将其添加到容器中
public Object instantiateItem(ViewGroup container, int position) {
container.addView(mViewList.get(position));
return mViewList.get(position);
}
// 获得指定页面项的标题文本
public CharSequence getPageTitle(int position) {
return mSoftwareList.get(position).name;
}
}
适配器负责具体页面项的管理,然后,Activity页面中有关的逻辑处理如下:
// 初始化翻页标题栏
private void initPagerStrip() {
// 从布局视图中获取名叫pts_tab的翻页标题栏
PagerTabStrip pts_tab = findViewById(R.id.pts_tab);
// 设置翻页标题栏的文本大小
pts_tab.setTextSize(TypedValue.COMPLEX_UNIT_SP, 40);
// 设置翻页标题栏的文本颜色
pts_tab.setTextColor(Color.BLUE);
}
private ArrayList<SoftwareBean> mSoftwareList = SoftwareBean.getDefaultList();
// 初始化翻页视图
private void initViewPager() {
// 构建一个工具软件图片的翻页适配器
ImagePagerAdapter adapter = new ImagePagerAdapter(this, mSoftwareList);
// 从布局视图中获取名叫vp_content的翻页视图
ViewPager vp_content = findViewById(R.id.vp_content);
// 给vp_content设置图片翻页适配器
vp_content.setAdapter(adapter);
// 设置vp_content默认显示第一个页面
vp_content.setCurrentItem(0);
// 给vp_content添加页面变化监听器
vp_content.addOnPageChangeListener(listener);
}
private ViewPager.OnPageChangeListener listener = new ViewPager.OnPageChangeListener() {
// 翻页状态改变时触发。arg0取值说明为:0表示静止,1表示正在滑动,2表示滑动完毕
// 在翻页过程中,状态值变化依次为:正在滑动→滑动完毕→静止
public void onPageScrollStateChanged(int arg0) {}
// 在翻页过程中触发。该方法的三个参数取值说明为 :第一个参数表示当前页面的序号
// 第二个参数表示当前页面偏移的百分比,取值为0到1;第三个参数表示当前页面的偏移距离
public void onPageScrolled(int arg0, float arg1, int arg2) {}
// 在翻页结束后触发。arg0表示当前滑到了哪一个页面
public void onPageSelected(int arg0) {
Toast.makeText(MainActivity.this, "当前页面项:"+mSoftwareList.get(arg0).name, Toast.LENGTH_SHORT).show();
}
};
当用户滑动至两个页面项中间时,效果如图:
四、对话框
1.提醒对话框
(1)作用和外观
Android提供了多种对话框,常见的有AlertDialog,ProgressDialog,DataPickerDialog,TimerPickerDialog这几类,对话框在页面最前层显示,它会抢占屏幕的焦点,用户无法操作对话框后面的界面。其中提醒对话框AlertDialog的扩展性是最大的,它也是其他对话框的父类,一个提醒对话框的外观如图,它分为4个区域:图标区,标题区,内容区,按钮区:
(2)使用方法
AlertDialog.Builder是提醒对话框的构造类,通过构造类可以最大程度的自定义一个具有特色的对话框,它的常用方法有:
①AlertDialog.Builder setIcon(Drawable icon)
AlertDialog.Builder setIcon(int iconId):设置图标区使用的图形,iconId代表图形资源的ID。
②AlertDialog.Builder setTitle(CharSequence title)
AlertDialog.Builder setTitle(int titleId):使用字符串或给定的资源ID设置标题区的文本。
③AlertDialog.Builder setMessage(int messageId)
AlertDialog.Builder setMessage(CharSequence message):设置要对话框内容区要显示的的文本消息。
④AlertDialog.Builder setPositiveButton(CharSequence text, DialogInterface.OnClickListener listener)
AlertDialog.Builder setNegativeButton(CharSequence text, DialogInterface.OnClickListener listener)
AlertDialog.Builder setNeutralButton(CharSequence text, DialogInterface.OnClickListener listener):创建“确定”,“取消”,“中性”按钮并设置按钮监听器,注意,不显式调用这些方法则表示不创建这些按钮。
⑤AlertDialog create():使用提供给此构建器的参数创建一个 AlertDialog 。
⑥AlertDialog.Builder setCancelable(boolean cancelable):设置点击对话框范围之外的屏幕时,对话框是否可以消失,默认是true,开发者如果要求用户必须做出选择,则设置为false。
⑦AlertDialog show():使用提供给此构建器的参数创建一个 AlertDialog ,并立即显示该对话框。
以上都是设置对话框内容的常规方法,提醒对话框还支持以下方法来自定义对话框的外观:
⑧AlertDialog.Builder setCustomTitle(View customTitleView):使用自定义视图 customTitleView设置标题区。
⑨AlertDialog.Builder setView(int layoutResId)
AlertDialog.Builder setView(View view):将对话框的内容区设置为自定义视图。
⑩AlertDialog.Builder setAdapter(ListAdapter adapter, DialogInterface.OnClickListener listener)
AlertDialog.Builder setItems(CharSequence[] items, DialogInterface.OnClickListener listener)
AlertDialog.Builder setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems, DialogInterface.OnMultiChoiceClickListener listener)
AlertDialog.Builder setSingleChoiceItems(CharSequence[] items, int checkedItem, DialogInterface.OnClickListener listener):对话框的内容区使用列表的方式来显示数据。
如此,在指定Activity页面显示一个提醒对话框的流程如下:
①首先创建一个AlertDialog.Builder的实例对象。
②然后调用它的setXXX方法来设置对话框四个不同区域的显示内容。
③最后调用它的create方法来生成一个AlertDialog的实例对象,调用AlertDialog对象的show方法即可显示对话框了,或者直接使用AlertDialog.Builder对象的show方法来代替这两句代码。
那么举个例子来熟悉自定义对话框的内容区,比如把内容区设置为列表的形式代码如下:
//获取内容区的数据来源
ArrayList<SoftwareBean> mSoftwareList = SoftwareBean.getDefaultList();
SoftwareAdapter adapter = new SoftwareAdapter(MainActivity.this,mSoftwareList);
private void initAlertDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
//设置对话框图标区
builder.setIcon(R.mipmap.ic_launcher)
//设置对话框按钮区,暂不处理按钮事件
.setPositiveButton("确定按钮",null)
.setNegativeButton("取消按钮",null)
.setNeutralButton("中间按钮",null)
//设置标题区
.setTitle("标题")
//设置内容区
.setAdapter(adapter, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String desc = String.format("您点击了第%d个工具软件,它的名字是%s", which + 1,
mSoftwareList.get(which).name);
Toast.makeText(MainActivity.this, desc, Toast.LENGTH_LONG).show();
}
})
.setCancelable(false);//必须与对话框交互才能让对话框消失
AlertDialog dialog =builder.create();
dialog.show();
}
显示效果如图,用户必须与对话框交互(点击按钮或列表项)才能让对话框消失:
2.进度对话框
(1)作用和外观
进度对话框ProgressDialog虽然在手册里被标记为过时的,但它的使用方法确实很简单,而且这个控件也比较实用,比如要求用户在进度完成之前无法对APP界面做任何操作,那么,使用ProgressDialog既可以显示进度又可以抢占屏幕的焦点。ProgressDialog继承自AlertDialog,故它的布局和提醒对话框是一样的,但它额外集成了进度条ProgressBar,于是在内容区就出现了一个进度条控件:
(2)使用方法
ProgressDialog拥有AlertDialog和ProgressBar的所有方法,故这里不再列举,但不同于AlertDialog的使用方式,进度对话框没有构造类来设置对话框内容,它直接通过构造函数来创建对象实例并通过对象实例的各种setXXX方法设置对话框的内容。
那么要实现上图的效果,需要设置的代码如下:
//初始化进度对话框并显示
private void initProgressDialog() {
ProgressDialog dialog = new ProgressDialog(MainActivity.this);
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);//条状的进度条
dialog.setMax(100);
dialog.setProgress(0);//设置进度条当前进度
dialog.setIcon(R.mipmap.ic_launcher);
dialog.setTitle("标题");
dialog.setMessage("对话框内容");
dialog.setButton(DialogInterface.BUTTON_NEGATIVE,"取消按钮", (DialogInterface.OnClickListener) null);
dialog.setButton(DialogInterface.BUTTON_POSITIVE,"确定按钮", (DialogInterface.OnClickListener) null);
dialog.setButton(DialogInterface.BUTTON_NEUTRAL,"中间按钮", (DialogInterface.OnClickListener) null);
dialog.setCancelable(false);//必须点击按钮才能让对话框消失
dialog.show();
}
3.日期选择对话框
(1)作用和外观
日期选择对话框DatePickerDialog相当于把日期选择器DatePicker以对话框的形式呈现给用户,可以为DatePickerDialog注册监听器来监听当用户选择日期事件,它的外观如图:
当选择好日期后,点击确定会触发监听器的onDateSet方法。
(2)使用方法
DatePickerDialog没有setXXX方法来设置日期内容,如果开发者不追求外观,则可以使用两句代码搞定。只需在构造函数中设置一下当前的日期并设置一个日期选择的监听器,就可以生成一个实例对象了,再调用它的show方法显示出来即可。常用方法如下:
①DatePickerDialog(Context context, DatePickerDialog.OnDateSetListener listener, int year, int month, int dayOfMonth):使用context的默认主题作为对话框布局,设置日期选择监听器,设置对话框显示的初始年月日。其中的监听器listener需要实现 abstract void onDateSet(DatePicker view, int year, int month, int dayOfMonth) 方法。
当然也可以动态设置日期和监听器:
②void setOnDateSetListener(DatePickerDialog.OnDateSetListener listener):为日期选择对话框设置监听器。
③void updateDate(int year, int month, int dayOfMonth):设置当前日期。
4.时间选择对话框
(1)作用和外观
时间选择对话框TimePickerDialog相当于把时间选择器 TimePicker以对话框的形式呈现给用户,不过,它默认不支持秒钟的设置,可以为它设置监听器来监听时间选择事件,它的外观如图:
点击红圈中的图标,可以为TimePickerDialog切换两种不同的外观。选择好时间后,点击确定会触发监听器的onTimeSet方法。
(2)使用方法
它的使用和日期选择对话框一样,开发者如果没有特殊的外观要求的话,可以用两句代码搞定。它的常用方法有:
①TimePickerDialog(Context context, TimePickerDialog.OnTimeSetListener listener, int hourOfDay, int minute, boolean is24HourView):使用context的默认主题来创建一个默认外观的时间选择器,注册一个监听器,设置对话框要显示的初始时间,参数is24HourView设置使用24小时或12小时的风格显示。监听器listener需要实现 public void onTimeSet(TimePicker view, int hourOfDay, int minute) 方法。
②void updateTime(int hourOfDay, int minuteOfHour):设置当前时间。
日期,时间选择对话框的使用都很简单,实现上图的效果需要的代码段如下:
Calendar calendar =Calendar.getInstance();//获取日历实例对象
//初始化日期选择对话框并显示
private void initDatePickerDialog(){
DatePickerDialog dialog = new DatePickerDialog(this, new DatePickerDialog.OnDateSetListener() {
@Override
public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
// 获取日期对话框设定的年月份
String desc = String.format("您选择的日期是%d年%d月%d日",
year, month + 1, dayOfMonth);
Toast.makeText(MainActivity.this, desc, Toast.LENGTH_SHORT).show();;
}
},calendar.get(Calendar.YEAR),calendar.get(Calendar.MONTH),calendar.get(Calendar.DAY_OF_MONTH));
dialog.show();
}
//初始化时间选择对话框并显示
private void initTimePickerDialog(){
TimePickerDialog dialog =new TimePickerDialog(this, new TimePickerDialog.OnTimeSetListener() {
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
// 获取时间对话框设定的时间
String desc = String.format("您选择的时间是%d时%d分",
hourOfDay, minute);
Toast.makeText(MainActivity.this, desc, Toast.LENGTH_SHORT).show();;
}
},calendar.get(Calendar.HOUR),calendar.get(Calendar.MINUTE),true);
dialog.show();
}
五、视频视图
(1)作用和外观
视频视图VideoView继承自SurfaceView而且内部集成了MediaPlayer。如同文本视图用于装载并显示文本,图像视图用于装载并显示图像,视频视图是专门用于播放视频的控件,它的默认外观是一片空白的矩形区域。
(2)使用方法
VideoView的方法主要是关于内部集成的MediaPlayer的方法,在前文硬件控制中已经详细总结过了,这里就再简单总结一下VideoView提供给开发者的常用方法:
①void setVideoPath(String path)
void setVideoURI(Uri uri, Map<String, String> headers)
void setVideoURI(Uri uri):通过不同的参数设置视频来源,可以是手机存储,可以是网络视频。
②void addSubtitleSource(InputStream is, MediaFormat format):添加来自输入流is的外部字幕源文件。注意,单个外部字幕源可能包含多个或不支持的音频轨道。
③void setOnCompletionListener(MediaPlayer.OnCompletionListener l)
void setOnErrorListener(MediaPlayer.OnErrorListener l)
void setOnInfoListener(MediaPlayer.OnInfoListener l)
void setOnPreparedListener(MediaPlayer.OnPreparedListener l):注册播放结束,播放错误,播放过程信息,准备播放的监听器。
④void seekTo(int msec):让视频跳到指定时长播放。
⑤int getBufferPercentage()
int getCurrentPosition()
int getDuration():获取已经缓冲的时长,当前时长,总时长。
⑥boolean onKeyDown(int keyCode, KeyEvent event)
boolean onTouchEvent(MotionEvent ev)
boolean onTrackballEvent(MotionEvent ev):与用户操作按键或屏幕有关的回调函数。
⑦boolean canPause()
boolean isPlaying()
void pause()
void resume()
void start()
void suspend():控制播放状态的几个方法。
⑧void setMediaController(MediaController controller)*:设置与VideoView绑定的媒体控制器MediaController ,如果直接调用此方法,则MediaController会自动附着在VideoView的下方。
媒体控制器MediaController支持基本的播放控制操作,如:显示播放进度,拖动到指定位置播放,暂停/恢复播放,查看播放总时长/已播放时长,快进/快退等,当它与视频视图绑定后,就可以直接和视频视图交互了。这些操作在MediaController通常以相关的图标表示,以此提供给用户直接控制视频视图。
由于MediaController是不可继承的,意味着开发者无法通过继承来改变控制器的外观或者扩展额外功能。MediaController主要是由用户直接操作,开发者也没必要做太多的处理,对于开发者来说,它的常用方法有:
①public void setAnchorView(View view) :绑定指定的视频视图。
②public void setMediaPlayer(MediaPlayerControl player):指定媒体播放器,效果和方法①一样,故①和②只需调用一个即可。
③public boolean isShowing()
public void show()
public void hide():与媒体控制器在屏幕上显示/隐藏相关的方法。
使用视频视图来播放视频的步骤如下:
开发者只需在布局中放置一个VideoView控件,在代码中调用setVideoPath方法指定视频文件,再调用setMediaController方法指定媒体控制器,媒体控制器无需在布局文件里添加,使用代码添加就会默认附着在VideoView的下方。最后调用MediaController对象的setAnchorView或setMediaPlayer方法来双向绑定即可。
那么按照以上流程,在布局文件中放置一个ID为vv_content的VideoView,然后很容易写出以下代码:
//初始化视频视图,绑定媒体控制器,播放视频
private void initVideoView(){
VideoView vv_content = findViewById(R.id.vv_content); // 获取一个视频视图对象
// 视频文件的完整路径
String file_path = "/mnt/sdcard/DCIM/Camera/VID_20211016_161752.mp4";
// 设置视频视图的视频路径
vv_content.setVideoPath(file_path);
// 视频视图请求获得焦点
vv_content.requestFocus();
// 创建一个媒体控制条
MediaController mc_play = new MediaController(this);
// 给视频视图设置相关联的媒体控制器
vv_content.setMediaController(mc_play);
// 给媒体控制器设置相关联的视频视图
mc_play.setMediaPlayer(vv_content);
// 视频视图开始播放
vv_content.start();
}
随手录制的视频VID_20211016_161752.mp4放在/mnt/sdcard/DCIM/Camera/目录下,例子实现的效果图如下,如果没有在配置文件声明存储权限,就会出现图左1的情况:
六、表面视图和纹理视图
在硬件控制的摄像头小节中,已经使用过了表面视图和纹理视图,但没有深入总结有关方法,在此本小结之前,希望大家还记着Java中的绘图过程和有关的API,这样会更加得心应手的掌握这两个视图。
1.表面视图
(1)作用和外观
表面视图SurfaceView外观和View一样,都是一个空白的矩形区域,开发者通常需要通过继承SurfaceView,然后在这片空白的区域绘制自定义的图形。而不同的是,SurfaceView提供了可以多线程同时绘制的画布。
(2)使用方法
表面视图一般与表面持有者SurfaceHolder结合使用,通过调用SurfaceView的getHolder方法可以获取与之关联的SurfaceHolder对象实例。
SurfaceHolder的常用方法有:
①abstract Canvas lockCanvas()
abstract Canvas lockCanvas(Rect dirty):锁定与之绑定的表面视图,不允许在该语句调用之后创建,销毁或修改表面视图,同时返回一个表面视图的用于绘制的画布Canvas,参数dirty表示获取表面视图的指定区域的画布。
②abstract void unlockCanvasAndPost(Canvas canvas):释放表面视图,并将unlockCanvasAndPost方法调用之前,lockCanvas方法调用之后的,对canvas的所有绘制显示在表面视图上。
③abstract boolean isCreating():表面视图是否有效,如果在其他线程里操作表面视图,则首先要判断它是否有效。
④abstract Surface getSurface():获取表面对象。
⑤abstract void setFixedSize(int width, int height):设置表面具的尺寸大小。
⑥abstract void setFormat(int format):设置表面的所需PixelFormat,通常取值OPAQUE(不透明),TRANSLUCENT(半透明),TRANSPARENT(透明)。
⑦abstract void addCallback(SurfaceHolder.Callback callback)
abstract void removeCallback(SurfaceHolder.Callback callback)
为此持有者添加/删除回调接口。接口callback需要实现以下三个方法:
①abstract void surfaceChanged(SurfaceHolder holder, int format, int width, int height):在表面进行任何更改(格式或大小)后立即调用。
②abstract void surfaceCreated(SurfaceHolder holder):在表面首次创建后立即调用。
③abstract void surfaceDestroyed(SurfaceHolder holder):在表面被销毁之前立即调用。
2.纹理视图
(1)作用,外观和使用方法
纹理视图TextureView也支持多线程绘制,和SurfaceView相比,它是View的子类,故可以使用View的视图变化方法,如:透明度,平移,旋转,设置背景等, 它其他的常用方法有:
①Canvas lockCanvas()
Canvas lockCanvas(Rect dirty):锁定纹理视图获取用于绘制的画布。
②void unlockCanvasAndPost(Canvas canvas):完成编辑表面中的像素。
③boolean isAvailable():表面纹理SurfaceTexture是否可用。
④SurfaceTexture getSurfaceTexture():返回纹理视图使用的表面纹理SurfaceTexture。
⑤void setSurfaceTextureListener(TextureView.SurfaceTextureListener listener):设置纹理视图的监听器,listener需要实现的方法有:
①abstract void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height):当TextureView的SurfaceTexture准备好使用时调用。
②abstract boolean onSurfaceTextureDestroyed(SurfaceTexture surface):当SurfaceTexture即将销毁时调用。
③abstract void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height):当 SurfaceTexture的缓冲区大小发生变化时调用。
④abstract void onSurfaceTextureUpdated(SurfaceTexture surface):当表面纹理更新时调用。
在多线程中绘制图形时,这两个视图控件本身没有什么奇怪的外观,开发者通常都是继承这两个控件类,在获取的画布上中绘制图形。核心流程就是在子线程中调用lockCanvas方法获取画布,然后绘制,绘制结束后,使用unlockCanvasAndPost方法刷新显示。
限于篇幅,这里就只用表面视图举个简单的例子,熟悉它的多线程绘制流程,首先自定义类DrawShapeSurfaceView 继承SurfaceView,在里面使用两个线程分别绘制圆形和矩形,代码如下:
public class DrawShapeSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
private final int INTERVAL_TIME = 100; // 绘制间隔(mS)
private final float PERCENTAGE_INTERVAL = 0.01f; //每次绘制的进度占总进度的1%
private Paint circlePaint, rectanglePaint; // 声明两个画笔对象
private RectF sumRectF, circleRectF, rectangleRectF; // 纹理视图,圆形,矩形占据的屏幕大小,
private float mEndAngle = 0.0f; // 绘制扇形需要的终点角度
private Path mPath; //绘制矩形所需的路径数据
private boolean isRunning = false; // 是否正在绘制
private final SurfaceHolder mHolder; // 声明一个表面持有者对象
public DrawShapeSurfaceView(Context context) {
this(context, null);
}
public DrawShapeSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
// 获取表面视图的表面持有者
mHolder = getHolder();
// 给表面持有者添加表面变更监听器
mHolder.addCallback(this);
// 下面两行设置背景为透明,因为SurfaceView默认背景是黑色
setZOrderOnTop(true);
mHolder.setFormat(PixelFormat.TRANSLUCENT);
}
@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 计算表面视图的内容实际可用的最大空间
int maxWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
// 根据最大宽度创建表面视图的内容区域的边界
sumRectF = new RectF(getPaddingLeft(), getPaddingTop(),
getPaddingLeft() + maxWidth, getPaddingTop() + maxWidth);
//圆形和矩形平分内容区域的边界
circleRectF = new RectF(sumRectF.left, sumRectF.centerY() - (sumRectF.centerX() - sumRectF.left) / 2,
sumRectF.centerX(), sumRectF.centerY() + (sumRectF.centerX() - sumRectF.left) / 2);
rectangleRectF = new RectF(sumRectF.centerX(), sumRectF.top, sumRectF.right, sumRectF.bottom);
}
// 开始绘制圆形和矩形
public void startDrawShape() {
isRunning = true;
// 绘制圆形的线程
new Thread() {
@Override
public void run() {
while (isRunning) {
// 计算扇形下一次绘制的起始角度,即当前起始角度+每次绘制的角度(percentageOfInterval * 360)
mEndAngle += PERCENTAGE_INTERVAL * 360;
if (mEndAngle > 360){
mEndAngle = 360;
// 当mBeginAngle值为360时表示圆形绘制结束
isRunning = false;
}
drawCircle(circlePaint, mEndAngle);
try {
Thread.sleep(INTERVAL_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
// 绘制矩形的线程
new Thread() {
@Override
public void run() {
//获取矩形的周长:(长+宽)*2
float perimeterOfRectangle = (rectangleRectF.height() + rectangleRectF.width()) *2;
//每次绘制的路径长度:单次绘制的比例*周长
float oneDrawingLength = PERCENTAGE_INTERVAL * perimeterOfRectangle;
//已经绘制的总长度
float sumOfDrawing = 0.0f;
//已完成本次绘制,计算下一次绘制的点的起始坐标
float nextX = rectangleRectF.left;
float nextY = rectangleRectF.top;
//将路径的起点定义在矩形的左上角的顶点
mPath = new Path();
mPath.setLastPoint(nextX,nextY);
while (isRunning) {
//顺时针绘制:上边->右边->下边->左边
sumOfDrawing += oneDrawingLength;//已经绘制的长度
if (sumOfDrawing <= rectangleRectF.width()){//绘制矩形上边
//Y坐标不变,X坐标加上绘制长度
nextX += oneDrawingLength;
nextY = rectangleRectF.top;
//连接本次绘制路径的终点
mPath.lineTo(nextX,nextY);
} else if ((sumOfDrawing > rectangleRectF.width())
&&(sumOfDrawing <= rectangleRectF.width() + rectangleRectF.height())){//绘制右边
//首先将右上角的顶点连接起来
mPath.lineTo(rectangleRectF.right, rectangleRectF.top);
//X坐标不变,Y坐标:矩形的右上顶点Y坐标+已经绘制的长度-上边
nextX = rectangleRectF.right;
nextY = rectangleRectF.top + (sumOfDrawing - rectangleRectF.width());
//连接本次绘制路径的终点
mPath.lineTo(nextX,nextY);
} else if ((sumOfDrawing > rectangleRectF.width() + rectangleRectF.height())
&&(sumOfDrawing <= rectangleRectF.width() * 2 + rectangleRectF.height())){//绘制下边
//首先将右下角的顶点连接起来
mPath.lineTo(rectangleRectF.right, rectangleRectF.bottom);
//Y坐标不变,X坐标:矩形的右下顶点X坐标-(已经绘制的长度-前两条边的总长)
nextX = rectangleRectF.right - (sumOfDrawing - (rectangleRectF.width() + rectangleRectF.height()));
nextY = rectangleRectF.bottom;
//连接本次绘制路径的终点
mPath.lineTo(nextX,nextY);
} else if ((sumOfDrawing > rectangleRectF.width() * 2 + rectangleRectF.height())
&&(sumOfDrawing <= perimeterOfRectangle)){//绘制左边
//首先将左下角的顶点连接起来
mPath.lineTo(rectangleRectF.left, rectangleRectF.bottom);
//X坐标不变,Y坐标:矩形的左下顶点Y坐标-(已经绘制的长度-前三条边的总长)
nextX = rectangleRectF.left;
nextY = rectangleRectF.bottom - (sumOfDrawing - (rectangleRectF.width() * 2 + rectangleRectF.height()));
//连接本次绘制路径的终点
mPath.lineTo(nextX,nextY);
} else if (sumOfDrawing > perimeterOfRectangle){//已经绘制完成
//首先将左上角的顶点连接起来
mPath.lineTo(rectangleRectF.left, rectangleRectF.top);
//在绘制结束后停止本线程
isRunning = false;
}
//绘制矩形的路径
drawRectangle(rectanglePaint, mPath);
try {
Thread.sleep(INTERVAL_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
// 停止转动
public void stopDrawShape() {
isRunning = false;
}
// 每隔一段时间以指定的角度绘制扇形,从而有一种动态绘制圆形的效果
private void drawCircle(Paint paint, float endAngle) {
// 因为两个线程都在绘制,所以这里利用同步机制,防止表面视图的资源被锁住
synchronized (mHolder) {
// 锁定表面持有者的画布对象
Canvas canvas = mHolder.lockCanvas();
if (canvas != null) {
// SurfaceView上次的绘图结果仍然保留,如果不想保留上次的绘图,则需将整个画布清空
// canvas.drawColor(Color.WHITE);
// 在画布上绘制扇形。第四个参数为true表示绘制扇形,为false表示绘制圆弧
canvas.drawArc(circleRectF, 0,endAngle, false, paint);
// 解锁表面持有者的画布对象
mHolder.unlockCanvasAndPost(canvas);
}
}
}
// 绘制图形
private void drawRectangle(Paint paint, Path path) {
// 因为两个线程都在绘制,所以这里利用同步机制,防止表面视图的资源被锁住
synchronized (mHolder) {
// 锁定表面持有者的画布对象
Canvas canvas = mHolder.lockCanvas();
if (canvas != null) {
// SurfaceView上次的绘图结果仍然保留,如果不想保留上次的绘图,则需将整个画布清空
// canvas.drawColor(Color.WHITE);
// 在画布上绘制矩形
canvas.drawPath(path,paint);
// 解锁表面持有者的画布对象
mHolder.unlockCanvasAndPost(canvas);
}
}
}
// 获取指定颜色的画笔
private Paint getPaint(int color,Paint.Style style) {
Paint paint = new Paint(); // 创建新画笔
paint.setAntiAlias(true); // 设置画笔为无锯齿
paint.setColor(color); // 设置画笔的颜色
paint.setStrokeWidth(10); // 设置画笔的线宽
paint.setStyle(style); // 设置画笔的类型。STROKE表示空心,FILL表示实心
return paint;
}
// 在表面视图创建时触发
public void surfaceCreated(SurfaceHolder holder) {
circlePaint = getPaint(Color.RED,Style.STROKE);
rectanglePaint = getPaint(Color.CYAN,Style.STROKE);
}
// 在表面视图变更时触发
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
// 在表面视图销毁时触发
public void surfaceDestroyed(SurfaceHolder holder) {}
}
以上代码中稍微复杂的是图形的绘制,其中圆形是由绘制多个圆弧而来,矩形则是计算绘制的长度来连接矩形路径。例子的页面布局很简单:一个复选框用于控制自定义表面视图的两个绘制线程和一个自定义的表面视图,如下:
复选框的逻辑处理如下:
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView.getId() == R.id.ck_control) {
if (isChecked) {
ck_control.setText("停止");
mView.startDrawShape(); // 表面视图开始绘制
} else {
ck_control.setText("开始");
mView.stopDrawShape(); // 表面视图停止绘制
}
}
}
多线程绘制的效果如图: