Android: 将 WebView 输出加载到 App Widget
Android: loading WebView output into App Widget
虽然我很欣赏 App Widget does not support a WebView
directly, is it at all possible to use an ImageView
(which is supported), and a technique like described here 为 ImageView
生成图像? WebView
不会直接使用,只是在后台使用,为ImageView
.
提供图像
这显然是可能的,至少在我的设备上是这样,尽管你的里程可能会有所不同。
在清单中,我们正常注册 Widget 的 <receiver>
,还注册处理 WebView
及其图像捕获的 Service
。
<manifest ... >
...
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application ... >
...
<receiver
android:name=".WebWidgetProvider"
android:label="@string/widget_name" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
<service android:name=".WebShotService" />
</application>
</manifest>
在 Widget 的信息文件中,widget_info.xml
,我已将最小尺寸设置为 4 x 2。
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_layout"
android:minWidth="292dip"
android:minHeight="146dip" />
对于此示例,Widget 的布局 widget_layout.xml
只是一个 ImageView.
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/ic_launcher" />
此示例中的 AppWidgetProvider
仅覆盖 onUpdate()
方法,因为我们使用相同的 ACTION_APPWIDGET_UPDATE
来触发我们自己的 AlarmManager
更新。请注意,只要有活动的小部件,示例警报就会每 30 秒触发一次。您可能不想在测试之外执行此操作,因为小部件更新可能相当昂贵。
public class WebWidgetProvider extends AppWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// We can't trust the appWidgetIds param here, as we're using
// ACTION_APPWIDGET_UPDATE to trigger our own updates, and
// Widgets might've been removed/added since the alarm was last set.
final int[] currentIds = appWidgetManager.getAppWidgetIds(
new ComponentName(context, WebWidgetProvider.class));
if (currentIds.length < 1) {
return;
}
// We attach the current Widget IDs to the alarm Intent to ensure its
// broadcast is correctly routed to onUpdate() when our AppWidgetProvider
// next receives it.
Intent iWidget = new Intent(context, WebWidgetProvider.class)
.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, currentIds);
PendingIntent pi = PendingIntent.getBroadcast(context, 0, iWidget, 0);
((AlarmManager) context.getSystemService(Context.ALARM_SERVICE))
.setExact(AlarmManager.RTC, System.currentTimeMillis() + 30000, pi);
Intent iService = new Intent(context, WebShotService.class);
context.startService(iService);
}
}
在 Service
中,我们实例化一个 WebView
,并将其添加到 FrameLayout
,后者又被添加到宽度为零的 WindowManager
和高度参数。然后我们在 WebView
中加载一个测试 URL,当页面完成时,我们强制 WebView
布局到适当的大小,并将其绘制到我们的目标 Bitmap
通过 Canvas
对象。这是在稍微延迟后完成的,以允许 WebView
完全呈现自身,以免我们最终得到一个空白的白色图像。然后在 RemoteViews
对象上设置 Bitmap
以更新 Widget 布局中的 ImageView
。我在代码中留下了一个 Toast
用于测试目的,因为我们的示例 Widget 可能比 Stack Overflow 的首页刷新更频繁。
public class WebShotService extends Service {
private WebView webView;
private WindowManager winManager;
public int onStartCommand(Intent intent, int flags, int startId) {
winManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
webView = new WebView(this);
webView.setVerticalScrollBarEnabled(false);
webView.setWebViewClient(client);
final WindowManager.LayoutParams params =
new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSLUCENT);
params.x = 0;
params.y = 0;
params.width = 0;
params.height = 0;
final FrameLayout frame = new FrameLayout(this);
frame.addView(webView);
winManager.addView(frame, params);
webView.loadUrl("http://whosebug.com");
return START_STICKY;
}
private final WebViewClient client = new WebViewClient() {
public void onPageFinished(WebView view, String url) {
final Point p = new Point();
winManager.getDefaultDisplay().getSize(p);
webView.measure(MeasureSpec.makeMeasureSpec((p.x < p.y ? p.y : p.x),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec((p.x < p.y ? p.x : p.y),
MeasureSpec.EXACTLY));
webView.layout(0, 0, webView.getMeasuredWidth(), webView.getMeasuredHeight());
webView.postDelayed(capture, 1000);
}
};
private final Runnable capture = new Runnable() {
@Override
public void run() {
try {
final Bitmap bmp = Bitmap.createBitmap(webView.getWidth(),
webView.getHeight(), Bitmap.Config.ARGB_8888);
final Canvas c = new Canvas(bmp);
webView.draw(c);
updateWidgets(bmp);
}
catch (IllegalArgumentException e) {
e.printStackTrace();
}
stopSelf();
}
};
private void updateWidgets(Bitmap bmp) {
final AppWidgetManager widgetManager = AppWidgetManager.getInstance(this);
final int[] ids = widgetManager.getAppWidgetIds(
new ComponentName(this, WebWidgetProvider.class));
if (ids.length < 1) {
return;
}
final RemoteViews views = new RemoteViews(getPackageName(), R.layout.widget_layout);
views.setImageViewBitmap(R.id.widget_image, bmp);
widgetManager.updateAppWidget(ids, views);
Toast.makeText(this, "WebWidget Update", 0).show();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
这是 Widgety 功能的屏幕截图。
添加到上面的答案,
对于遇到此错误的人 - 无法添加 window。此 window 类型 的权限被拒绝,至少在我使用设备 运行 marshmallow 的情况下,需要更改类型。
只需替换
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY 到
WindowManager.LayoutParams.TYPE_TOAST(或)
WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
它会很好地工作。
注:用Nexus6p运行确认最新版本的marsmallow。
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY 需要权限 android.permission.INTERNAL_SYSTEM_WINDOW 只能为 android 添加系统相关应用。
对于希望使用 Javascript.
解析数据的用户,在 onPageFinished() 完成页面加载后,不要忘记更改 Web 视图的高度和宽度
我在上面的回答中完全忽略了这一点的重要性,结果浪费了很多时间。
最初我的假设是因为我的要求不是通过图像视图将 Web 视图中的内容显示到小部件,这不是必需的。
事实证明 javascript 无法解析来自 Web 视图的数据,除非视图宽度和高度不为零。
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 仅在 Android M 之后有效。
并且需要配置权限 Settings.canDrawOverlays(context)
虽然我很欣赏 App Widget does not support a WebView
directly, is it at all possible to use an ImageView
(which is supported), and a technique like described here 为 ImageView
生成图像? WebView
不会直接使用,只是在后台使用,为ImageView
.
这显然是可能的,至少在我的设备上是这样,尽管你的里程可能会有所不同。
在清单中,我们正常注册 Widget 的 <receiver>
,还注册处理 WebView
及其图像捕获的 Service
。
<manifest ... >
...
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application ... >
...
<receiver
android:name=".WebWidgetProvider"
android:label="@string/widget_name" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
<service android:name=".WebShotService" />
</application>
</manifest>
在 Widget 的信息文件中,widget_info.xml
,我已将最小尺寸设置为 4 x 2。
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_layout"
android:minWidth="292dip"
android:minHeight="146dip" />
对于此示例,Widget 的布局 widget_layout.xml
只是一个 ImageView.
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/ic_launcher" />
此示例中的 AppWidgetProvider
仅覆盖 onUpdate()
方法,因为我们使用相同的 ACTION_APPWIDGET_UPDATE
来触发我们自己的 AlarmManager
更新。请注意,只要有活动的小部件,示例警报就会每 30 秒触发一次。您可能不想在测试之外执行此操作,因为小部件更新可能相当昂贵。
public class WebWidgetProvider extends AppWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// We can't trust the appWidgetIds param here, as we're using
// ACTION_APPWIDGET_UPDATE to trigger our own updates, and
// Widgets might've been removed/added since the alarm was last set.
final int[] currentIds = appWidgetManager.getAppWidgetIds(
new ComponentName(context, WebWidgetProvider.class));
if (currentIds.length < 1) {
return;
}
// We attach the current Widget IDs to the alarm Intent to ensure its
// broadcast is correctly routed to onUpdate() when our AppWidgetProvider
// next receives it.
Intent iWidget = new Intent(context, WebWidgetProvider.class)
.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, currentIds);
PendingIntent pi = PendingIntent.getBroadcast(context, 0, iWidget, 0);
((AlarmManager) context.getSystemService(Context.ALARM_SERVICE))
.setExact(AlarmManager.RTC, System.currentTimeMillis() + 30000, pi);
Intent iService = new Intent(context, WebShotService.class);
context.startService(iService);
}
}
在 Service
中,我们实例化一个 WebView
,并将其添加到 FrameLayout
,后者又被添加到宽度为零的 WindowManager
和高度参数。然后我们在 WebView
中加载一个测试 URL,当页面完成时,我们强制 WebView
布局到适当的大小,并将其绘制到我们的目标 Bitmap
通过 Canvas
对象。这是在稍微延迟后完成的,以允许 WebView
完全呈现自身,以免我们最终得到一个空白的白色图像。然后在 RemoteViews
对象上设置 Bitmap
以更新 Widget 布局中的 ImageView
。我在代码中留下了一个 Toast
用于测试目的,因为我们的示例 Widget 可能比 Stack Overflow 的首页刷新更频繁。
public class WebShotService extends Service {
private WebView webView;
private WindowManager winManager;
public int onStartCommand(Intent intent, int flags, int startId) {
winManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
webView = new WebView(this);
webView.setVerticalScrollBarEnabled(false);
webView.setWebViewClient(client);
final WindowManager.LayoutParams params =
new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSLUCENT);
params.x = 0;
params.y = 0;
params.width = 0;
params.height = 0;
final FrameLayout frame = new FrameLayout(this);
frame.addView(webView);
winManager.addView(frame, params);
webView.loadUrl("http://whosebug.com");
return START_STICKY;
}
private final WebViewClient client = new WebViewClient() {
public void onPageFinished(WebView view, String url) {
final Point p = new Point();
winManager.getDefaultDisplay().getSize(p);
webView.measure(MeasureSpec.makeMeasureSpec((p.x < p.y ? p.y : p.x),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec((p.x < p.y ? p.x : p.y),
MeasureSpec.EXACTLY));
webView.layout(0, 0, webView.getMeasuredWidth(), webView.getMeasuredHeight());
webView.postDelayed(capture, 1000);
}
};
private final Runnable capture = new Runnable() {
@Override
public void run() {
try {
final Bitmap bmp = Bitmap.createBitmap(webView.getWidth(),
webView.getHeight(), Bitmap.Config.ARGB_8888);
final Canvas c = new Canvas(bmp);
webView.draw(c);
updateWidgets(bmp);
}
catch (IllegalArgumentException e) {
e.printStackTrace();
}
stopSelf();
}
};
private void updateWidgets(Bitmap bmp) {
final AppWidgetManager widgetManager = AppWidgetManager.getInstance(this);
final int[] ids = widgetManager.getAppWidgetIds(
new ComponentName(this, WebWidgetProvider.class));
if (ids.length < 1) {
return;
}
final RemoteViews views = new RemoteViews(getPackageName(), R.layout.widget_layout);
views.setImageViewBitmap(R.id.widget_image, bmp);
widgetManager.updateAppWidget(ids, views);
Toast.makeText(this, "WebWidget Update", 0).show();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
这是 Widgety 功能的屏幕截图。
添加到上面的答案,
对于遇到此错误的人 - 无法添加 window。此 window 类型 的权限被拒绝,至少在我使用设备 运行 marshmallow 的情况下,需要更改类型。
只需替换
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY 到
WindowManager.LayoutParams.TYPE_TOAST(或)
WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
它会很好地工作。
注:用Nexus6p运行确认最新版本的marsmallow。
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY 需要权限 android.permission.INTERNAL_SYSTEM_WINDOW 只能为 android 添加系统相关应用。
对于希望使用 Javascript.
解析数据的用户,在 onPageFinished() 完成页面加载后,不要忘记更改 Web 视图的高度和宽度我在上面的回答中完全忽略了这一点的重要性,结果浪费了很多时间。
最初我的假设是因为我的要求不是通过图像视图将 Web 视图中的内容显示到小部件,这不是必需的。
事实证明 javascript 无法解析来自 Web 视图的数据,除非视图宽度和高度不为零。
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 仅在 Android M 之后有效。 并且需要配置权限 Settings.canDrawOverlays(context)