Android开发 - 高级用户体验
关键词:分辨率、Text-toSpeech、语音识别、振动、使用动画、SurfaceView、交互式操作、复制、粘贴和剪切板
- 分辨率无关和为每种屏幕做设计
- 在XML中创见图像资源
- 让应用程序可访问
- 使用Text-to-Speech和语音识别库
- 使用动画
- 控制硬件加速
- 使用Surface View
- 复制、粘贴和剪切板
为每个屏幕尺寸和分辨率做设计
- res/drawable-ldp 为120dpi左右的屏幕提供低密度资源。
- res/drawable-mdpi 为160dpi左右的屏幕提供中等密度资源。
- res/drawable-hdpi 为240dpi左右的屏幕提供高密度资源。
- res/drawable-tvdpi 为213dpi左右的屏幕提供中高密度资源。这是在API版本13中为了优化面向电视的应用程序而引入的。
- res/drawable-xhdpi 为320dpi左右的屏幕提供超高密度资源。
- res/drawable-nodpi 用于不管宿主屏幕如何都不能缩放的资源。
创建可缩放的布局
为不同的屏幕类型优化布局
使用long和notlong修饰符为正常的宽屏提供优化的布局,使用port和land修饰符分别指示当屏幕为竖屏或者横屏时所使用的布局。
- res/layout-long-land 为宽屏横屏模式提供布局
- res/layout-notlong-port 为费宽屏竖屏模式提供布局
基于当前屏幕的宽度/高度或者最小可用屏幕宽度:
- res/layout-w600dp
- res/layout-h720dp
- res/layout-sw320dp
如果打算让应用程序兼容早期的Android版本,最好连同small、medium、large和xlarge一起使用修饰符。
- res/layout-small
- res/layout-normal
- res/layout-large
- res/layout-xlarge
指定屏幕尺寸
对于一些应用程序,可能无法通过优化UI来使其支持所有可能的屏幕尺寸。可以通过在清单文件中使用supports-screens元素来指定应用程序可以运行在哪些屏幕上。
Android3.2(API level 13)为supports-screen节点引入了requiresSmallestWithDp、compatibleWidthLimitDp和largestWidthLimitDP属性。
创建可缩放的图形资源
创建优化的、自适应的、动态的设计
反复测试
确保可访问性
Android Text-to-Speech
Text-to-Speech允许你在应用程序中输出合成的语音,让应用程序和用户“交谈”。
由于一些Android设备的存储空间的限制,语言包不总是预置到每个设备上。在使用TTS引擎之前,最好确认已经安装了语言包。
要想检查TTS库,可以启动一个新的带返回结果的Activity,并传入TextToSpeech.Engine类的ACTION_CHECK_TTS_DATA操作。
private static int TTS_DATA_CHECK = 1; private void initTextToSpeech() { Intent intent = new Intent(Engine.ACTION_CHECK_TTS_DATA); startActivityForResult(intent, TTS_DATA_CHECK); }
如果已经成功安装语音数据,onActivityResult方法回收到Engine.CHECK_VOICE_DATA_PASS。如果当前语音数据不可用,可以使用TTS Engine类的ACTION_INSTALL_TTS_DATA操作启动一个新的Activity以初始化它的安装。
protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == TTS_DATA_CHECK) { if (resultCode == Engine.CHECK_VOICE_DATA_PASS) { // Do Something } else { Intent installVoice = new Intent(Engine.ACTION_INSTALL_TTS_DATA); startActivity(installVoice); } } }
确认了语音数据可用之后,需要创建和初始化一个新的TextToSpeech实例。在构造函数中传入一个OnInitListener,当TTS引擎初始化完成后会回调它。
tts = new TextToSpeech(this, new OnInitListener() { public void onInit(int status) { if (status == TextToSpeech.SUCCESS) { ttsIsInit = true; if (tts.isLanguageAvailable(Locale.UK) >= 0) tts.setLanguage(Locale.UK); tts.setPitch(0.8f); tts.setSpeechRate(1.1f); speak(); } } });
当TextToSppech初始化完成后,可以使用speak方法通过默认的设备音频输出同步音频数据:
if (tts != null && ttsIsInit) { tts.speak("Hello, Android", TextToSpeech.QUEUE_ADD, null); }
通过setPitch和setSpeechRate方法,可以影响语音输出声音的方式。每个方法接受一个浮点数,分别用于修改语言输出的高音和音速。
通过setLanguage方法,还可以改变语言输出的发音。这个方法需要一个Locale参数来制定所说的文本的国家和语言,这会影响文本的发音方式,以确保能够使用正确的语言和发音模型。
发音完毕后,使用stop方法停止语音输出,使用shutdown方法来释放TTS资源。
private TextToSpeech tts = null; private boolean ttsIsInit = false; private void initTextToSpeech() { Intent intent = new Intent(Engine.ACTION_CHECK_TTS_DATA); startActivityForResult(intent, TTS_DATA_CHECK); } protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == TTS_DATA_CHECK) { if (resultCode == Engine.CHECK_VOICE_DATA_PASS) { tts = new TextToSpeech(this, new OnInitListener() { public void onInit(int status) { if (status == TextToSpeech.SUCCESS) { ttsIsInit = true; if (tts.isLanguageAvailable(Locale.UK) >= 0) tts.setLanguage(Locale.UK); tts.setPitch(0.8f); tts.setSpeechRate(1.1f); speak(); } } }); } else { Intent installVoice = new Intent(Engine.ACTION_INSTALL_TTS_DATA); startActivity(installVoice); } } } private void speak() { if (tts != null && ttsIsInit) { tts.speak("Hello, Android", TextToSpeech.QUEUE_ADD, null); } } @Override public void onDestroy() { if (tts != null) { tts.stop(); tts.shutdown(); } super.onDestroy(); }
使用语音识别
Android通过使用RecognizerIntent类来支持语音输入和语音识别。
要想初始化语音识别,需要调用startActivityForResult,并传入指定了RecognizerIntent.ACTION_RECOGNIZE_SPEECH或者RecognizerIntent.ACTION_WEB_SEARCH操作的Intent。
RecognizerIntent
RecognizerIntent.EXTRA_LANGUAGE_MODEL额外项以指定用于分析输入音频的语言模型。
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); // 指定自由形式的输入 intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); // 指定字符串,将会在语音输入对话框中显示,以提示用户开始讲话 intent.putExtra(RecognizerIntent.EXTRA_PROMPT,"or forever hold your peace"); // 最大识别字符数量 intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1); // 指定输入的语言为英文 intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.ENGLISH); startActivityForResult(intent, VOICE_RECOGNITION);
当用户完成语音输入后,作为结果的音频将会由语音识别引擎分析和处理。结果将通过onActivityResult处理程序返回,作为EXTRA_RESULTS额外项中的字符串ArrayList。
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == VOICE_RECOGNITION && resultCode == RESULT_OK) { ArrayList<String> results; results = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); float[] confidence; String confidenceExtra = RecognizerIntent.EXTRA_CONFIDENCE_SCORES;
confidence = data.getFloatArrayExtra(confidenceExtra); // TODO Do something with the recognized voice strings } super.onActivityResult(requestCode, resultCode, data); }
ArrayList中每个返回的字符串都代表语音输入的一种可能匹配。在每个结果中,可以找到语音识别引擎的信任值,这个值在RecognizerIntent.EXTRA_CONFIDENCE_SCORES额外项中以浮点型数组的形式返回。该数组的值是介于0(无信任)和1(高信任)的值,代表了正确识别语音的程度。
使用语音识别进行搜索
可以通过RecognizerIntent.ACTION_WEB_SEARCH操作显示一个网络搜索结果,或者基于用户的语音来触发其他类型的语音操作,而不是自己处理接收到的语音。
Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); startActivityForResult(intent, 0);
控制设备震动
要想控制设备的振动,应用程序需要有VIBRATE权限。
<user-permission android:name="android.permission.VIBRATE"/>
设备的振动是通过Vibrator Service来控制的,通过getSystemService方法获取该服务
String vibratorService = Context.VIBRATOR_SERVICE;
Vibrator vibrator = (Vibrator)getSystemService(vibratorService);
long[] pattern = (1000,2000,4000,8000,16000}; vibrator.vibrate(pattern, 0); //使用振动方式 vibrator.vibrate(1000); //振动一秒钟 // 取消振动 vibrator.cancel();
使用动画
补间View动画
补间动画常用于:
- Activity间的转换。
- Activity内的布局间的转换。
- 相同View中的不同内容间的转换。
- 为用户提供反馈,例如提示进度、通过“晃动”输入框来说明错误或者无效的数据输入。
补间动画是使用Animation类创建的。
AlphaAnimation 可以改变View的透明度。
RotateAnimation 可以在XY平面上旋转选中的View Canvas。
ScaleAnimation 允许缩放选中的View。
TranslateAnimation 可以再屏幕上移动选中的View(但是它只能在其原始边界的范围内显示)
Android提供了AnimationSet类来对动画进行分组和配置,从而让它们作为一个集合运行。
定义补间动画
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator"> <scale android:fromXScale="0.0" android:toXScale="1.0" android:fromYScale="0.0" android:toYScale="1.0" android:pivotX="50%" android:pivotY="50%" android:duration="1000" /> </set>
应用补间动画
//设置重复模式 myAnimation.setRepeatMode(Animation.RESTART); //设置重复次数 myAnimation.setRepeatCount(Animation.INFINITE); //将动画应用到任意View中 myView.startAnimation(myAnimation);
使用动画监听器
对一个Animation对象调用setAnimationListener来对Animation进行监听。
myAnimation.setAnimationListener(new AnimationListener() { public void onAnimationEnd(Animation animation) { //TODO: 动画完成后执行处理 } public void onAnimationStart(Animation animation) { //TODO:动画开始时执行处理 } public void onAnimationRepeat(Animation animation) { //TODO:动画重复时执行处理 } }
为布局和View Group添加动画
创建布局动画
要创建一个新的布局动画,首先要定义一个将应用于每个子View的动画。然后,在代码中或者作为外部动画资源,创见一个新的LayoutAnimation,它引用了要应用的动画并应用它的顺序和时间安排。
以下示例展示了一个布局动画定义。当把布局动画分配到任意ViewGroup,它会随机地讲一个简单的“弹入(pop-in)”动画应用于ViewGroup的每一个子View。
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android" android:delay="0.5" android:animationOrder="random" android:animation="@anim/popin" />
使用布局动画
一旦定义了一个布局动画,就可以使用布局定义中的android:layoutAnimation来将其应用到一个ViewGroup中。
android:layoutAnimation="@anim/popinlayout"
要在代码中设置一个布局动画,可以对ViewGroup调用setLayoutAnimation,并给它传递所希望应用于LayoutAnimation对象的引用。
通常情况下,布局动画会在ViewGroup第一次进行布局的时候执行一次。
也可以通过的ViewGroup对象调用scheduleLayoutAnimation来强制它再次执行。
myViewGroup.setLayoutAnimationListener(new AnimationListener() { public void onAnimationEnd(Animation _animation) { } public void onAnimationRepeat(Animation _animation) { } public void onAnimationStart(Animation _animation) { } }
创建和使用逐帧动画
补间动画使用目标View来提供动画的内容,而逐帧动画则可以让你指定一系列用作View的背景的Drawable对象。
AnimationDrawable类可以用来创见一个新的表示为Drawable资源的逐帧动画。
也可以使用XML,在应用程序的res/drawable文件夹下将动画Drawable资源定义为外部资源。
使用animation-list标记来分组一个item节点集合,其中每一个item节点都是用一个drawable属性来定义要显示的图像,并且使用一个duration属性来制定显示它的时间(以毫秒为单位)。
以下示例创建了一个简单的动画来演示火箭的起飞(不包含火箭的图像)。
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false"> <item android:drawable="@drawable/rocket1" android:duration="500" /> <item android:drawable="@drawable/rocket2" android:duration="500" /> <item android:drawable="@drawable/rocket3" android:duration="500" /> </animation-list>
要显示动画,可以通过使用setBackgroundResource方法将其设置为一个View的背景。
ImageView image = (ImageView)findViewById(R.id.my_animation_frame);
image.setBackgroundResource(R.drawable.animated_rocket);
或者,可以利用setBackgroundDrawable方法来使用一个Drawable实例而不是一个资源引用。然后调用它的start方法运行动画。
AnimationDrawable animation = (AnimationDrawable)image.getBackground();
animation.start();
插值属性动画
补间View动画虽然改变了它所作用的View的外观,但并没有改变对象的本事。而属性动画却直接改变了它所作用的对象的属性。
可以在视觉或者其他方面修改任意对象的任意属性,通过一个属性动画生成器,在一个给定时间内使用你选择的差值算法将该属性从一个值转换为另一个值,如果需要的话,还可以设置动画的重复性行为。这个值可以是任意对象,从常规的整型到复杂的类对象实例都可以。
强化View
高级Canvas绘图
硬件加速
<application android:hardwareAccelerated="true">
要想为一个指定的Activity启用或者禁用硬件加速,可以在Activity的清单节点中使用相同的属性。
<activity android:name=".MyActivity" android:hardwareAccelerated="false" />
为一个Activity内的特定View启用或禁用硬件加速。
myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
检测硬件加速是否可用
@Override public void onDraw(Canvas canvas) { if(canvas.isHardwarAccelerated()) { //硬件加速的绘图逻辑 } else { //非硬件加速的绘图逻辑 } }
Surface View
在一般情况下,应用程序的View都是在相同的GUI线程中绘制的。这个主应用程序线程同时也用来处理所有的用户交互。
对于一个View的onDraw方法来说,是不能被移到后台线程中的。因为从后台线程修改一个GUI元素会被显式的禁止。
当要快速更新View的UI,或者当渲染代码阻塞GUI线程的时间过长的时候,SurfaceView就是解决上述问题的最佳选择。SurfaceView封装了一个Surface对象而不是画布。因为可以使用后台线程绘制Surface。
独立于GUI线程进行绘图的代价是会增加内存消耗。所以,虽然它是创建自定义View的有效方式 - 有事甚至是必需的。但在使用SurfaceView的时候仍然要保持谨慎。
创建SurfaceView
要创建一个新的SurfaceView,需要创见一个新的扩展SurfaceView的类,并实现SurfaceHolder Callback。
SurfaceHolder回调可以在底层的Surface被创建、销毁和修改的时候通知View,并传递给它对SurfaceHolder对象的引用,其中包含了当前有效地Surface。
一个典型的SurfaceView设计方式包括一个有Thread所派生的类,它可以接受对当前的SurfaceHolder的引用,并独立的更新它。
以下示例展示了使用画布所绘制的SurfaceView的实现。在SurfaceView控件中创建一个新的由Thread派生的类,并且所有的UI更新都是在这个新类中处理的。
import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.SurfaceHolder; import android.view.SurfaceView; public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback { private SurfaceHolder holder; private MySurfaceViewThread mySurfaceViewThread; private boolean hasSurface; public MySurfaceView(Context context) { super(context); init(); } public MySurfaceView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public MySurfaceView(Context context, AttributeSet ats, int defaultStyle) { super(context, ats, defaultStyle); init(); } private void init() { // 创建一个新的SurfaceHolder,并分配这个类作为它的回调 holder = getHolder(); holder.addCallback(this); hasSurface = false; } public void resume() { // 创建并启动图形更新线程 if (mySurfaceViewThread == null) { mySurfaceViewThread = new MySurfaceViewThread(); if (hasSurface == true) mySurfaceViewThread.start(); } } public void pause() { // 结束图形更新线程 if (mySurfaceViewThread != null) { mySurfaceViewThread.requestExitAndWait(); mySurfaceViewThread = null; } } public void surfaceCreated(SurfaceHolder holder) { hasSurface = true; if (mySurfaceViewThread != null) mySurfaceViewThread.start(); } public void surfaceDestroyed(SurfaceHolder holder) { hasSurface = false; pause(); } public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { if (mySurfaceViewThread != null) mySurfaceViewThread.onWindowResize(w, h); } class MySurfaceViewThread extends Thread { private boolean done; MySurfaceViewThread() { super(); done = false; } @Override public void run() { SurfaceHolder surfaceHolder = holder; // 重复绘图循环,知道线程停止 while (!done) { // 锁定Surface,并返回要在其上绘制的画布 Canvas canvas = surfaceHolder.lockCanvas(); // TODO: 在画布上绘图 // 解锁画布,并渲染当前的图像 surfaceHolder.unlockCanvasAndPost(canvas); } } public void requestExitAndWait() { // 把这个线程标记为完成,并合并到主应用程序线程 done = true; try { join(); } catch (InterruptedException ex) { } } public void onWindowResize(int w, int h) { // 处理可用Surface尺寸的改变 } } }
创建交互式控件
1.使用触摸屏
要创建一个使用触摸屏进行交互的View或者Activity,需要重写onTouchEvent处理程序
@Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); }
如果已经处理了屏幕触摸,就返回true;否则,就返回false。从而通过一个View和Activity的栈来传递事件,直到成功地处理触摸事件为止。
处理单点和多点触摸事件
MotionEvent
对MotionEvent参数调用getAction可以找出触发了该处理程序的事件类型。
调用getPointerCount可以找出触摸事件是否是一个多点触摸事件。
跟踪触摸移动
每当当前的触摸接触位置、压力或者区域大小发生改变的时候,一个新的onTouchEvent就会通过一个ACTION_MOVE操作触发。
MotionEvent参数还包含历史值。这个历史值代表了上次处理的onTouchEvent和这一次的onTouchEvent事件之间发生的所有移动事件,它允许Android缓冲快速的移动该表,从而提供对移动数据的细粒度的捕获。
可以通过调用getHistorySize来获得历史的大小值,该办法可以返回当前事件可用的移动位置的数量,然后你可以通过使用一系列getHistorySize*方法并传递给它位置索引来获得每一个历史事件的时间、压力、大小和位置。
@Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case (MotionEvent.ACTION_MOVE): { int historySize = event.getHistorySize(); for (int i = 0; i < historySize; i++) { float x = event.getHistoricalX(i); float y = event.getHistoricalY(i); processMovement(x, y); } float x = event.getX(); float y = event.getY(); processMovement(x, y); return true; } } return super.onTouchEvent(event); } private void processMovement(float _x, float _y) { //在移动的时候做点事情 }
使用onTouchListener
可以通过使用setOnTouchListener方法将OnTouchListener附加到任意的View对象来监听触摸事件,而不需要为已有的View创建派生类。
button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { //响应移动事件 return false; } });
使用设备按键、按钮和D-pad
@Override public boolean onKeyDown(int keyCode, KeyEvent event) { //执行按下按键处理,如果已经处理,就返回true return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { //执行释放按键处理,如果已经处理,就返回true return super.onKeyUp(keyCode, event); }
KeyEvent
使用onKeyListener
要在Activity的View中对按键的按下作出响应,需要实现OnKeyListener,并使用setOnKeyListener方法将其分配给一个View。
OnKeyListener并不是为单独的按键按下和释放分别独立的方法,而是使用单一的onKey事件。
button.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { //处理按键按下事件,如果已经处理,就返回true return false; } });
可以通过KeyEvent类中静态的keyCode值与参数的keyCode来找到按键。
使用轨迹球
轨迹球事件是通过重写View或者Activity中的onTrackballEvent方法来处理的。
@Override public boolean onTrackballEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); //处理轨迹球移动 return super.onTrackballEvent(event); }
高级Drawable资源
复制、粘贴和剪切板
ClipboardManager clipboardManager=(ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
剪贴板支持文本字符串、URI(通常是只想一个Content Provider项)和Intent(用于复制应用程序的快捷方式)。
要想向剪贴板复制一个对象,可以创建一个新的ClipData对象,它包含一个描述了与待复制对象相关的元数据的ClipDescription、任意数量的ClipData.Item对象。
使用setPrimaryClip方法将ClipData添加到剪贴板上。
ClipData clipData=ClipData.newPlainText("copied text","Hello Android!");
clipboardManager.setPrimaryClip(clipData);
在任意时刻,剪贴板中只能包含一个ClipData对象。复制一个新的对象会替换之前持有的剪贴板对象。
1.向剪贴板中复制数据
ClipData
static ClipData |
newHtmlText(CharSequence label, CharSequence text, String htmlText)
Create a new ClipData holding data of the type |
static ClipData |
newIntent(CharSequence label, Intent intent)
Create a new ClipData holding an Intent with MIME type |
static ClipData |
newPlainText(CharSequence label, CharSequence text)
Create a new ClipData holding data of the type |
static ClipData |
newRawUri(CharSequence label, Uri uri)
Create a new ClipData holding an URI with MIME type |
static ClipData |
newUri(ContentResolver resolver, CharSequence label, Uri uri)
Create a new ClipData holding a URI. |
2.粘贴剪贴板数据
//判断剪贴板中是否有数据 if(!clipboardManager.hasPrimaryClip()){ //剪贴板没有数据,禁止粘贴 }
还可以查询当前剪贴板中的ClipData对象的数据类型。
使用getPrimaryClipDescription方法获得剪贴板数据中的元数据,并使用它的hasMimeType方法指定应用程序粘贴所支持的MIME类型。
// 判断剪贴板中是否有数据 if (!clipboardManager.hasPrimaryClip()) { // 剪贴板没有数据,禁止粘贴 } if (!clipboardManager.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN)) { // 如果剪贴板中的内容是一个不支持的类型,就禁用粘贴UI选项 } else { // 如果剪贴板中的内容是一个支持的类型,就启用粘贴UI选项 } // 要想访问数据本身,可以使用getItemAt方法,传入你要遍历的项的索引值 ClipData.Item item = clipboardManager.getPrimaryClip().getItemAt(0); // 通过分别使用getText、getUri和getIntent方法,可以获取文本、URI和Intent。 CharSequence pasteData = item.getText(); Intent pasteIntent = item.getIntent(); Uri pasteUri = item.getUri(); String pasteString = item.getHtmlText(); // 任意剪贴板项的内容也是可以粘贴的,即使应用程序只支持文本。 // 使用coerceToText方法,可以将ClipData.Item对象的内容转化为一个字符串 CharSequence pasteText = item.coerceToText(this);
ClipDescription
String |
MIMETYPE_TEXT_HTML
The MIME type for a clip holding HTML text. |
String |
MIMETYPE_TEXT_INTENT
The MIME type for a clip holding an Intent. |
String |
MIMETYPE_TEXT_PLAIN
The MIME type for a clip holding plain text. |
String |
MIMETYPE_TEXT_URILIST
The MIME type for a clip holding one or more URIs. |

关键词:分辨率、Text-toSpeech、语音识别、振动、使用动画、SurfaceView、交互式操作、复制、粘贴和剪切板
浙公网安备 33010602011771号