Android RemoteViews 11问11答
1.什么是RemoteView?
答:其实就是一种特殊的view结构,这种view 能够跨进程传输。并且这种remoteview 还提供了一些方法 可以跨进程更新界面。具体在android里面 一个是通知 一个是桌面小部件。
这2个就是remoteview 最直接的应用了
2.RemoteView在通知上的应用?
答:这里给出一个小例子,比较粗糙 仅做演示使用。
//默认样式的notification
private void normalStyleNotification() {
Intent intent = new Intent(MainActivity.this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(MainActivity.this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification.Builder builder = new Notification.Builder(MainActivity.this);
Notification notification = builder.setContentIntent(pendingIntent).setContentTitle("title").setContentText("text").setSmallIcon(R.mipmap.ic_launcher).build();
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(1, notification);
} //自定义样式
private void customStyleNotification() {
Intent intent = new Intent(MainActivity.this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(MainActivity.this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification.Builder builder = new Notification.Builder(MainActivity.this);
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.notification_layout);
remoteViews.setTextViewText(R.id.tv, "自定义样式的文本");
remoteViews.setImageViewResource(R.id.iv, R.mipmap.ic_launcher);
Notification notification = builder.setSmallIcon(R.mipmap.ic_launcher).setContentIntent(pendingIntent).setContent(remoteViews).build();
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(2, notification);
}
效果如下:
3.小部件开发 大概流程如何?
答:android的小部件开发就全都是用的remoteviews。其实就是一个广播接收器+ui显示 ,诸如下图显示:
启动某个小部件以后就可以看到:
下面给出一个简单的例子 ,示范下小部件的基本开发流程,其实非常简单,毕竟就只是一个广播而已。只不过在具体app中 小部件或许会变的非常复杂。
既然是广播,我们就首先要给他一个接收器,保证能接收到广播:
<receiver android:name=".MyWidgetProvider">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info"></meta-data>
<intent-filter>
<!--这个是单击事件的action-->
<action android:name="com.example.administrator.remoteviewtest.CLICK"></action>
<!--这个必须要有 属于默认需要添加的-->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"></action>
</intent-filter>
</receiver>
然后在res/xml下 建立我们的widget配置文件:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/layout_widget"
android:minHeight="100dp"
android:minWidth="200dp"
android:updatePeriodMillis="160000"
></appwidget-provider>
然后写一个最简单的widget 的 layout文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="@android:color/holo_red_light"
android:layout_height="match_parent"
android:orientation="vertical"> <ImageView
android:id="@+id/iv2"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/shuqi" /> </LinearLayout>
最后 我们来编写我们的appwidgetProvider: 只完成一件事 就是点击他以后 图片就翻转
package com.example.administrator.remoteviewtest; import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.os.SystemClock;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast; /**
* Created by Administrator on 2016/2/5.
*/
public class MyWidgetProvider extends AppWidgetProvider { public static final String CLICK_ACTION = "com.example.administrator.remoteviewtest.CLICK"; @Override
public void onReceive(final Context context, Intent intent) {
super.onReceive(context, intent);
if (intent.getAction().equals(CLICK_ACTION)) {
Toast.makeText(context, "收到单击事件", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() { Bitmap srcBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.shuqi);
AppWidgetManager appwidgetManager = AppWidgetManager.getInstance(context);
//点击以后就让图片不断旋转
for (int i = 0; i < 37; i++) {
float degree = (i * 10) % 360;
RemoteViews remoteviews = new RemoteViews(context.getPackageName(), R.layout.layout_widget);
remoteviews.setImageViewBitmap(R.id.iv2, rotateBitmap(context, srcBitmap, degree));
//每次更新的时候 都别忘记了给他添加点击事件
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteviews.setOnClickPendingIntent(R.id.iv2, pendingIntent);
appwidgetManager.updateAppWidget(new ComponentName(context, MyWidgetProvider.class), remoteviews);
SystemClock.sleep(30);
}
}
}).start();
}
} @Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
//这个地方couter 其实就是你widget 布局里面 带有id的 view的总数,比如我们这个例子里面就只有
//一个imageview 有id 所以这个地方counter的值就是1
final int counter = appWidgetIds.length;
for (int i = 0; i < counter; i++) {
int appWidgetId = appWidgetIds[i];
//每次添加小部件或者小部件自己有更新时,我们都要重新更新小部件里面点击事件 等view相关的资源
updateWidget(context, appWidgetManager, appWidgetId);
}
} private void updateWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
RemoteViews remoteviews = new RemoteViews(context.getPackageName(), R.layout.layout_widget); //单击小部件以后发生的事
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteviews.setOnClickPendingIntent(R.id.iv2, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, remoteviews);
} //将图片翻转一定的角度
private Bitmap rotateBitmap(Context context, Bitmap srcBitmap, float degree) {
Matrix matrix = new Matrix();
matrix.reset();
matrix.setRotate(degree);
Bitmap dstBitmap = Bitmap.createBitmap(srcBitmap, 0, 0, srcBitmap.getWidth(), srcBitmap.getHeight(), matrix, true);
return dstBitmap;
} }
最后看一下简单的效果:
4.简述一下 widget开发中 几个重要的生命周期函数 都在什么情况下被调用?
答:onEnable :当widget第一次被添加到桌面时调用,添加行为可以多次,但是这个函数只有在第一次时能得到执行。
onUpdate:widget被添加或者更新时就会执行
onDeleted:删除的时候执行
onDisabled:最后一个该类型的widget被删除的时候 调用他
onReceive:分发广播的。我们主要的逻辑一般都写在这里。
5.intent和PendingIntent有什么区别。
答:intent代表的行为 是马上就要发生。而pendingIntent代表 这个行为是在将来的某个不确定的时间发生。 我们给RemoteView 添加点击事件的时候 都用pendingIntent。
pendingIntent通过set和cancel 方法来发送和取消 那些intent。
6.pendingIntent 支持哪几种?
答:三种。分别是getActivity getService 和getBroadcast。
7.pendingIntent的匹配规则是什么?
答:内部的intent和requestCode 都相同的时候 系统就认为这2个pendingIntent是相同的。intent相同的判定原则是 intent-filter和componentName相同。
8.如何理解pendingIntent的 flags 标志位?
答:可以从通知的角度来理解。manager.notify(1, notification); 我们平常发通知的时候 这个函数的 第一个参数 ,我们一般都是写一个常量。注意notification里面是有pendingintent的。
当我们这一个参数是写死的常量的时候 那不管notification里面的pendingIntent 是什么,这个通知都是永远老的被新的直接替代。
如果这notify的第一个参数 每次都不相同,情况就复杂的多,要分开来看:
notify函数的第一个参数不相同,notification里的pendingintent不相同时:通知和通知之间互相不干扰,可以看看效果:
notify函数的第一个参数不相同,notification里的pendingintent相同时,这个时候就要看flags的参数了。
如果flag=FLAG_ONE_SHOT,那后续的pendingIntent就和第一个保持一致,哪怕是intent里面的参数extras 都是一致的。
而且如果点击任何一个通知,其他的通知都无法打开了。
flag=FLAG_CANCEL_CURRENT 只有最新的通知才能打开,以前的通知点击都无效无法打开。
flag=FLAG_UPDATE_CURRENT 之前的通知会被更新成 和最新的那条通知是一样的 里面的extras都是一样的。并且都可以打开。
9.remoteView 支持所有的view类型吗?
答:remoteview 仅仅支持少部分系统自带的view。开发者的自定义view 是一律都不支持的,具体支持哪些系统的view和viewgroup,可以自行谷歌。
10.简述remoteViews的 原理?
答:通知栏和widget 都是运行在 NotificationManagerService和AppWidgetService的,并且是在systemt server进程中。我们在apk代码里
使用notificationmanager或者appwidgetmanager实际上都是通过binder 来进行与system server的进程间通信的。
public class RemoteViews implements Parcelable, Filter {
可以看到remotviews是继承了Parcelable接口。所以这个remoteviews会通过binder 传递到system server 这个系统进程里。然后系统进程
会解析这个remotview 拿到他的包名和布局文件等信息 然后就通过layoutInflater 来加载这个布局了。
对于system server来说 remoteview就好像是自己这个进程里的 资源一样,而对于调用者 也就是我们自己的apk程序员来说,这个view
就显然是remoteviews 远程的了。并且值得注意的是,view的更新操作 是由action对象的apply方法来执行的!这一点千万要记住。
我们可以从源码的角度来逐一分析。
//假设我们remoteviews是调用了这个方法 我们就来看看这个方法的过程
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
} //这里就明白了 你看 到这还没有实际调用view的方法,而是调用了addAciton方法 好像是新增了一个action
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
} //这个方法就很简单 就是remoteviews内部有一个mActions对象 他是一个list 每次我们调用remoteviews的set方法的时候
//实际上都是往这个列表里面 新增了一个action
private void addAction(Action a) {
if (hasLandscapeAndPortraitLayouts()) {
throw new RuntimeException("RemoteViews specifying separate landscape and portrait" +
" layouts cannot be modified. Instead, fully configure the landscape and" +
" portrait layouts individually before constructing the combined layout.");
}
if (mActions == null) {
mActions = new ArrayList<Action>();
}
mActions.add(a); // update the memory usage stats
a.updateMemoryUsageEstimate(mMemoryUsageCounter);
} //真正的执行view的方法 就是在apply函数里
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context); View result;
// RemoteViews may be built by an application installed in another
// user. So build a context that loads resources from that user but
// still returns the current users userId so settings like data / time formats
// are loaded without requiring cross user persmissions.
final Context contextForResources = getContextForResources(context);
Context inflationContext = new ContextWrapper(context) {
@Override
public Resources getResources() {
return contextForResources.getResources();
}
@Override
public Resources.Theme getTheme() {
return contextForResources.getTheme();
}
@Override
public String getPackageName() {
return contextForResources.getPackageName();
}
}; LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); // Clone inflater so we load resources from correct context and
// we don't add a filter to the static version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
result = inflater.inflate(rvToApply.getLayoutId(), parent, false);
//前面的就是加载资源而已 真正的调用 还是在perform函数里
rvToApply.performApply(result, parent, handler); return result;
} //每次remoteviews的apply方法 实际上就是遍历里面的action 然后调用每个action的 applay方法
private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
if (mActions != null) {
handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
final int count = mActions.size();
for (int i = 0; i < count; i++) {
Action a = mActions.get(i);
a.apply(v, parent, handler);
}
}
} //继续跟 我们会发现action是一个抽象类 他的apply方法要交给他的子类自己去实现
private abstract static class Action implements Parcelable {
public abstract void apply(View root, ViewGroup rootParent,
OnClickHandler handler) throws ActionException;
//我们就看看ReflectionAction 这个子类的实现吧
//很明显的 最终的反射调用 都是由子类自己来实现的,action的子类很多 有兴趣可以自己查
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final View view = root.findViewById(viewId);
if (view == null) return; Class<?> param = getParameterType();
if (param == null) {
throw new ActionException("bad type: " + this.type);
} try {
getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
} catch (ActionException e) {
throw e;
} catch (Exception ex) {
throw new ActionException(ex);
}
}
基本流程就是这样,另外注意apply和reapply的区别,后者只更新界面 而前者还要加载布局。
11.除了 通知和widget 我们还能怎样使用remoteviews?
答:remoteview 既然是被设计用来 跨进程 更新ui的。所有跨进程更新ui的 场景都可以使用他来做,这里给出2个范例。
第一个范例:同一个apk下,2个进程 ,其中一个activity另外一个activity的ui。
首先我们看主activity,就是用广播接收一个remoteviews 然后显示出来而已
package com.example.administrator.remoteviewsameproject; import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.RemoteViews; public class MainActivity extends AppCompatActivity { private Button bt; private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override
public void onReceive(Context context, Intent intent) {
RemoteViews remoteViews = intent.getParcelableExtra("remoteViewsFlag");
if (remoteViews != null) {
View view = remoteViews.apply(MainActivity.this, remoteViewsLayout);
remoteViewsLayout.addView(view);
}
}
}; private LinearLayout remoteViewsLayout; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
registerReceiver(mBroadcastReceiver,new IntentFilter("updateRemoteview"));
remoteViewsLayout = (LinearLayout) findViewById(R.id.ll);
bt = (Button) findViewById(R.id.bt);
bt.setOnClickListener(new View.OnClickListener() { @Override
public void onClick(View v) {
Intent intent = new Intent();
intent.setClass(MainActivity.this, OtherActivity.class);
startActivity(intent);
}
}); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
} @Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
} @Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId(); //noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
} return super.onOptionsItemSelected(item);
}
}
然后看运行在另外一个进程里的activity 这个activity负责发送remoteviews,注意你们自己写的时候 要在manifest里 更改android:process的值
package com.example.administrator.remoteviewsameproject; import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.RemoteViews;
import android.widget.TextView; public class OtherActivity extends AppCompatActivity { private TextView tv; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_other);
tv=(TextView)findViewById(R.id.tv);
tv.setOnClickListener(new View.OnClickListener() { @Override
public void onClick(View v) {
RemoteViews remoteViews=new RemoteViews(getPackageName(),R.layout.content_other);
remoteViews.setTextViewText(R.id.tv,"虽然是otherActivity的布局 但是我显示在mainActivity了");
PendingIntent pendingIntent=PendingIntent.getActivity(OtherActivity.this,0,new Intent(OtherActivity.this,MainActivity.class),PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.tv,pendingIntent);
Intent intent=new Intent("updateRemoteview");
intent.putExtra("remoteViewsFlag",remoteViews);
sendBroadcast(intent); }
}); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
} }
然后看一下运行效果:
然后考虑第二种场景,就是不同apk 不同进程之间用remoteview 更新ui的例子。
因为不同apk的话,资源id是不同的,所以remoteview更新的时候 方法要改变一下。
你看我们上面的例子,取view 什么的 都是直接通过id 因为是一个apk么。
但是你想一下 2个apk的时候,你的id 和我的id 怎么可能一样?所以这种情况
我们2个apk 就要事先约定好remoteview里面的 资源名称。
然后我们在 根据名称 查找对应的布局文件 再进行加载。
针对这种情况 实际上我们只要修改主activity的代码即可
public void onReceive(Context context, Intent intent) {
RemoteViews remoteViews = intent.getParcelableExtra("remoteViewsFlag");
if (remoteViews != null) {
int layoutId = getResources().getIdentifier("content_main", "layout", getPackageName());
ViewGroup view =(ViewGroup) getLayoutInflater().inflate(layoutId, remoteViewsLayout, false);
remoteViews.reapply(MainActivity.this, view);
remoteViewsLayout.addView(view); }
}
然后我们就可以看到 另外一个apk的 remoteview 显示在我们的主apk的主页面里了
Android RemoteViews 11问11答的更多相关文章
- Android 动画 6问6答
1.view 动画有哪些需要注意的? 答:view动画 本身比较简单.http://www.cnblogs.com/punkisnotdead/p/5179115.html 看这篇文章的第五问就可以了 ...
- Android Window 9问9答
1.简述一下window是什么?在android体系里 扮演什么角色? 答:window就是一个抽象类,他的实现类是phoneWindow.我们一般通过windowManager 来访问window. ...
- OpenGL快问快答
OpenGL快问快答 本文内容主要来自对(http://www.opengl.org/wiki/FAQ)的翻译,随机加入了本人的观点.与原文相比,章节未必完整,含义未必雷同,顺序未必一致.仅供参考. ...
- sql注入之你问我答小知识
/*每日更新,珍惜少年时博客*/ 1.问:为啥order by 是排序.而在注入当中后面加的却不是字段而是数字捏? 答:第N个字段嘛.order by 5就是第五个字段,如果5存在,6不存在.就说明只 ...
- Oracle百问百答(二)
Oracle百问百答(二) 11. nvl函数有什么用? NVL( string1, replace_with) 功能:如果string1为NULL,则NVL函数返回replace_with的值,否则 ...
- k3 Bos开发百问百答
K/3 BOS开发百问百答 (版本:V1.1) K3产品市场部 目录 一.基础资料篇__ 1 [摘要]bos基础资料的显示问题_ 1 [摘要]单 ...
- Java 面试题问与答:编译时与运行时
Java 面试题问与答:编译时与运行时 2012/12/17 | 分类: 基础技术, 职业生涯 | 5 条评论 | 标签: RUNTIME, 面试 分享到:58 本文作者: ImportNew - 朱 ...
- 2016年11月11日 星期五 --出埃及记 Exodus 20:2
2016年11月11日 星期五 --出埃及记 Exodus 20:2 "I am the LORD your God, who brought you out of Egypt, out o ...
- Oracle百问百答(四)
Oracle百问百答(四) 31.怎样查看某用户下的表? select table_name from all_tables where owner=upper('jhemr'); 32.怎样查看某用 ...
随机推荐
- Node 出现 uncaughtException 之后的优雅退出方案
Node 的异步特性是它最大的魅力,但是在带来便利的同时也带来了不少麻烦和坑,错误捕获就是一个.由于 Node 的异步特性,导致我们无法使用 try/catch 来捕获回调函数中的异常,例如: try ...
- Template
创建win32应用程序空工程 //main.cpp//time: 01/08/2013 #include<d3d9.h>#include <d3dx9.h> #pragma c ...
- C++名字空间/C++命名空间
0.序言 名字空间是C++提供的一种解决符号名字冲突的方法. 一个命令空间是一个作用域,在不同名字空间中命名相同的符号代表不同的实体. 通常,利用定义名字空间的办法,可以使模块划分更加方便,减少模块间 ...
- jquery.lazyload用法
lazyload是jquery的插件,作为延迟加载图片,减压服务器压力. 如何使用: 先把 <script src="jquery.js" type="text/j ...
- Linux命令-sudo
sudo命令用于给普通用户提供额外权利来完成原本只有超级用户才有权限完成的任务, 格式:sudo [参数] 命令名称 sudo命令与su命令的区别是,su命令允许普通用户完全变更为超级管理员的身份,但 ...
- intelliJ IDEA中项目以jar包的形式导出
在上一篇中把intelliJ IDEA安装并配置完事后,我们就可以写scala程序了.编写完scala程序后我们要把程序导出,上传到服务器上,在spark集群下运行,下面就讲一下包的导出过程以及包在服 ...
- Oracle Demo ->> CREATE TABLE
Demo One CREATE TABLE employees_demo ( employee_id ) , first_name ) , last_name ) CONSTRAINT emp_las ...
- 【python爬虫】根据查询词爬取网站返回结果
最近在做语义方面的问题,需要反义词.就在网上找反义词大全之类的,但是大多不全,没有我想要的.然后就找相关的网站,发现了http://fanyici.xpcha.com/5f7x868lizu.html ...
- Linux系统安装MySQL步骤及支持远程操作配置方法
一.数据库安装(安装在/usr/local目录) 1. 压缩包拷贝到/users/lengyufang/tools 2. groupadd mysql3. useradd -r -g mysql -s ...
- LinuxMint使用中文输入法
自从转战linux系统以来,最痛苦的事情就是没有一款能让我满意的中文输入法了 不过今天我终于发现了一个让我比较满意的输入法(小小输入法),真的很不错 我试过不少输入法,但是还是小小输入法最适合我: 搜 ...