Discussion on custom Widget

catalogue

1, Preface

2, Basic use of Widget

2.1 AppWidgetProvider inheritance class object

2.2 AppWidgetProviderInfo resource configuration file

3, Customized demand

3.1 dilemma

3.2 custom widget scheme

Scheme 1: Drawable alternative drawing scheme

Scheme 2: add a custom View file scheme to the widget directory of the Framework

1, Preface

In the development of Android, it is sometimes encountered that widgets displayed on the launcher are customized according to the needs of the application, so that users can enjoy some functional services provided by the application without opening the application interface, such as common music players, picture display of gallery, clock and weather, etc. this document aims to introduce the basic use of widgets at present and discuss the limitations of the existing framework, What are our solutions to these limitations.

2, Basic use of Widget

Let's first introduce the simple widget configuration with a simple use case.

There are at least two steps to create an application widget:

2.1 AppWidgetProvider inheritance class object

You need to write a class that inherits from the AppWidgetProvider class:

class MyAppWidget: AppWidgetProvider(){
    override fun update(...){}
}

Generally speaking, you can rewrite the update method. When adding a widget or other operations trigger an update, you will receive an update broadcast. Then you will call this method. You create a RemoteViews object and configure it. Finally, you will call appwidgetmanager The updateappwidget method updates the widget UI. This RemoteViews is not a View in nature. It is an object specially used for cross process transmission. It saves the layout information of the widget. We will talk about it in detail later. In addition, this class also has some other methods, such as the delete callback onDeleted, the size adjustment callback onAppWidgetOPtionsChanged, and so on.

Don't forget to declare this class in the manifest file AndroidManifest. Although he is a provider, he is not a ContentProvider, but a Broadcast, so he needs to declare:

//AndroidManifest
<receiver android:name=".MyAppWidget"
    android:exported="true">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>    //Listen to the update broadcast sent by AppWidgetManager, just declare this
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/app_widget_my"/>    //This xml file is the AppWidgetProviderInfo configuration file, see the following section
</receiver>

There is a service called AppWidgetService in the system service. The specific logic is handled in AppWidgetServiceImpl. This service is used to manage widgets and is held by AppWidgetManager. It will listen to the broadcast of application installation:

private void registerBroadcastReceiver() {
    // Register for broadcasts about package install, etc., so we can
    // update the provider list.        
    IntentFilter packageFilter = new IntentFilter();        
    packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);        
    packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);        
    packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
    ......
}

Therefore, after the application is installed, AppWidgetServiceImpl will query the provider information declared by the application through the packageManager and create AppWidgetProviderInfo Object.

Two attributes need to be specified:

  • android:name - specifies the metadata name. Use android.appwidget The provider identifies the data as the AppWidgetProviderInfo descriptor
  • android:resource - specify AppWidgetProviderInfo resource location

2.2 AppWidgetProviderInfo resource configuration file

This is the Widget resource file declared in the manifest file above, which is saved in the res/xml folder of the project. Metadata used to describe the application Widget, such as the initial layout, update frequency, preset size, display position, etc. of the Widget. The system Widget service creates the AppWidgetProviderInfo object by reading this.

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"    //Minimum width
    android:minHeight="40dp"    //Minimum height
    android:targetCellWidth="5"    //The number of cells occupied by the preset width. The width and height of this cell are determined by the launcher
    android:targetCellHeight="2"    //Number of cells occupied by preset height
    android:updatePeriodMillis="86400000"   // The update frequency is specified to be at least 30 minutes, so it's very embarrassing in the actual use scenario. It's useless
    android:previewImage="@drawable/preview"      //Preview image seen when selecting widget
    android:initialLayout="@layout/example_appwidget"    //Initial layout
    android:configure="com.example.android.ExampleAppWidgetConfigure"    //Configuration activity to start when adding widget (optional)
    android:resizeMode="horizontal|vertical"    //User manually resizable direction
    android:widgetCategory="home_screen">    //Location of widget display, home screen (home_screen), keyguard (invalid for Android 5.0 or above)
</appwidget-provider>

After the above files are configured, the launcher application, as a widget hosting application, gets the AppWidgetProviderInfo configuration option from the framework and provides the service of embedding widgets in the interface. Therefore, our application does not communicate directly with the launcher, but communicates through the framework. The specific source code process analysis will be sorted out next time. Our focus this time is on the next link.

3, Customized demand

The above can only meet the basic needs of some widget development, and sometimes product design requires us to customize some more cool UI and interaction, at which time we encounter difficulties.

3.1 dilemma

Our three-party applications package the locally written widget xml layout into RemoteViews and transfer it to AppWidgetManager according to AppWidgetId. Then the AppWidgetService inside AppWidgetManager creates an AppWidgetProviderInfo object according to RemoteViews and saves it in the internal ArrayList. Then it will notify the widget hosting application (Launcher), The managed application will get the latest series of AppWidgetProviderInfo through AppWidgetManager for refresh.

This series of processes spans at least three different processes, so the RemoteViews serialized object is used to save the layout resource LayoutId, and then the View is obtained by inflating when it needs to be loaded later. This leads to the fact that the custom View we applied cannot be used because the system_ The server process cannot find the class defined in your application, so using custom View will result in an error, and the system cannot find the class information.

Caused by: android.view.InflateException: Binary XML file line #7 in com.example.widgettest:layout/test_widget_layout: Error inflating class com.example.widgettest.MyImageView
    Caused by: java.lang.ClassNotFoundException: com.example.widgettest.MyImageView
        at java.lang.Class.classForName(Native Method)
        at java.lang.Class.forName(Class.java:454)
        at android.view.LayoutInflater.createView(LayoutInflater.java:819)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1010)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:965)
        at android.view.LayoutInflater.rInflate(LayoutInflater.java:1127)
        at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1088)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:686)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:538)
        at android.widget.RemoteViews.inflateView(RemoteViews.java:5652)
        at android.widget.RemoteViews.-$$Nest$minflateView(Unknown Source:0)
        at android.widget.RemoteViews$AsyncApplyTask.doInBackground(RemoteViews.java:5779)
        at android.widget.RemoteViews$AsyncApplyTask.doInBackground(RemoteViews.java:5738)
        at android.os.AsyncTask$3.call(AsyncTask.java:394)
        at java.util.concurrent.FutureTask.run(FutureTask.java:264)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
        at java.lang.Thread.run(Thread.java:1012)

You may be curious about how the system process can load a View through a LayoutId for the inflate view operation mentioned in the underline above. After all, the resource ID is only unique within the application, but not necessarily unique throughout the system. The correspondence between the ID and the resource file is saved in the R file of the application. How does the external process confirm which layout file to load through the ID? In fact, when the application creates RemoteViews, it will save the ApplicationInfo object of our application process internally. Then, when the launcher needs a View, it will pass the launcher's context to the system process. The system process calls the context using this context and the ApplicationInfo object obtained earlier The createapplicationcontext method gets the context context of our application, and we can use the resource ID to access the resources of the target application through this context.

The views supported by RemoteViews are also very limited. According to the description of Google development documents,

The RemoteViews object can support the following layout classes:

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout

And the following View classes (excluding descendant classes):

  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView
  • ViewFlipper
  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper

And ViewStub.

Therefore, this restriction greatly affects our ability to play on the Widget. Many of the expected cool dynamic effects and interactions cannot be realized. For example, the Widget display of the satellite zenith map that I am going to do can not carry the display content of the satellite zenith map in the above View, because we constantly monitor the changes of the satellite trajectory, And draw according to the parameters such as the azimuth of the satellite and the rotation vector parameter value of the mobile phone. This is the biggest difficulty in the development of widgets. The extensibility of the native Widget framework is too poor.

Therefore, in the face of this situation where we need to dynamically draw content according to parameters instead of displaying fixed content, we need to find another way (unless you can abandon the existing widget framework and create a new framework that can support expansion. We can discuss this in the future, and students with ideas can share it).

3.2 custom widget scheme

Scheme 1: Drawable alternative drawing scheme

Considering that when we design a custom View, it is usually designed to be similar to a black box. Only some parameters need to be passed in externally, and the View will be drawn internally in the draw method according to the parameters, in this case, we can take a chance, replace the View that needs to be drawn internally with ImageView, and move these custom drawing operations to the custom drawable, Finally, when updating the Widget, call the RemoteViews.setImageViewBitmap method to convert the custom drawable into a Bitmap object and pass it to the user to update.

class TestDrawable: Drawable() {
    @ColorInt
    private var mColor = Color.BLUE
    override fun draw(p0: Canvas) {
        p0.drawColor(mColor)
    }
   fun updateColor(@ColorInt color: Int){
        mColor = color
    }
    ......
}
class TestAppWidget: AppWidgetProvider() {
    override fun onUpdate(context: Context?, appWidgetManager: AppWidgetManager?, appWidgetIds: IntArray?) {
        ......
        for (appWidgetId in appWidgetIds){
            val remoteView = RemoteViews(context.packageName, R.layout.test_widget_layout)
            remoteView.setImageViewBitmap(R.id.image_view, TestDrawable().toBitmap(200,200))    //The picture is blue by default
            val intent = PendingIntent.getBroadcast(context, 0, TestWidgetManager.getInternalUpdateIntent(context), PendingIntent.FLAG_MUTABLE)
            remoteView.setOnClickPendingIntent(R.id.image_view, intent)    //Set the intent of the click event. Here is a custom broadcast PendingIntent
            appWidgetManager?.updateAppWidget(appWidgetId, remoteView)
        }
    }    
    override fun onReceive(context: Context?, intent: Intent?) {
        ......
        //After receiving the customized click event broadcast, we can take the initiative to update the widget and transfer the new drawable
        if (intent?.action == TEST_UPDATE_APP_WIDGET){
            val appWidgetManager = AppWidgetManager.getInstance(context)
            val ids = appWidgetManager.getAppWidgetIds(ComponentName("com.example.widgettest","com.example.widgettest.TestAppWidget"))
            for (id in ids){
                val remoteView = RemoteViews(context.packageName, R.layout.test_widget_layout)
                val drawable = TestDrawable().apply { updateColor(Color.RED) }    //Turn the picture red
                remoteView.setImageViewBitmap(R.id.image_view, drawable.toBitmap(200,200))
                appWidgetManager.partiallyUpdateAppWidget(id, remoteView) //Local update
            }
        }
}
//test_widget_layout.xml
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <ImageView
        android:id="@+id/image_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/white"
        android:scaleType="centerCrop"
        android:clickable="true"/>
</FrameLayout>

We have set a click event broadcast notification for the widget's ImageView. When the user clicks on the widget's ImageView, it will change from the default blue to red. The actual effect is as follows:

It can be found that this idea is feasible. We can carry out more complex drawing operations in drawable according to product requirements, and realize the intersection of interaction and UI through the continuous coverage of drawable. You can even write an animation in drawable.

This method is not recommended when frequent updates are required. The widget communication itself involves cross process communication. In addition, the need to create Bitmap objects causes memory consumption, and frequent updates will affect battery power.

Scheme 2: add a custom View file scheme to the widget directory of the Framework

To really realize more UI and interaction possibilities, we can only put our custom View file in the framework/base/core/java/android/widget directory at present, so that the system service will not find our custom View when inflating our widget layout file.

However, it should be noted that our custom View needs to follow the following principles:

  • "High cohesion and low coupling" and other classes should be decoupled as much as possible. Custom classes that cannot be decoupled can only be placed in the framework directory, such as custom adapter s, tool classes, etc
  • The custom View class must be annotated with @ RemoteView, as shown below:
@RemoteView
public class MyImageView extends ImageView { ...... }
  • The paths are consistent. The path of the user-defined View applied by us needs to be the same as the path of the user-defined View placed in the system, that is, we need to add the same folder path: framework/base/core/java/android/widget under the java directory of our application project, and then put the user-defined View in it.

In the custom View, you can add data processing logic, View switching animation, and so on.

Now the system can recognize our custom View, but we can't directly inject data into the custom View in our own application. After all, when updating, there is only LayoutId and no View. We can't get the View object through findViewById and then inject the data. What should we do? RemoteViews also provides us with a series of methods:

//RemoteView.java
public class RemoteViews implements Parcelable, Filter {
    public void setBoolean(int viewId, String methodName, boolean value) {...}
    public void setColor(int viewId, @NonNull String methodName, int colorResource) {...}
    public void setString(int viewId, String methodName, String value) {...}    
    public void setBundle(int viewId, String methodName, Bundle value) {...}
    ......
}

From the parameters, you can probably guess how the data is injected. Yes, it is transmitted internally through reflection:

//RemoteView.BaseReflectionAction.apply(...)
getMethod(view, this.methodName, param, false /* async */).invoke(view, value);

Therefore, when you need to trigger the update actively or passively, you can call the above method when creating the RemoteViews object, and finally call the AppWidgetManager.updateAppWidget update method or the appwidgetmanager.partiallyupdaeappwidget local update method to refresh the UI of the widget.

Of course, if you want to customize some collection displays and customize adapters, it needs to be more complicated, because the existence of similar adapters has been encapsulated in the native framework, which has been written dead, and there are more places to modify. I won't talk about it here. I have made similar picture library widgets before, which can realize custom switching animation. Because it needs to modify some caching strategies inside the native adapter, it is a painful memory to change them. Haha.

The above are two schemes to realize custom drawing of widgets, one is custom Drawable and the other is custom View. Although the former does not need to modify the source code, it still has many limitations. Although the latter is relatively free and can achieve a more cool self-defined UI and interaction, you must have the ability to modify the source code, such as mobile phone system developers.

Tags: Java Android Front-end kotlin

Posted by pmaonline on Fri, 09 Sep 2022 22:45:04 +0530