Creating Custom Widgets in Badge Magic Android

In this blog, we are going to have a look on how I created this badge preview in fossasia/badge-magic-android


What is Canvas?

Canvas is a class in Android that performs 2D drawing of different objects onto the screen. The saying “a blank canvas” is very similar to what a Canvas object is on Android. It is basically, an empty space to draw onto.

Canvas Coordinate System

The coordinate system of the Android canvas starts in the top left corner, where [0,0] represents that point. The y axis is positive downwards, and x axis positive towards the right.


Some basics of Canvas, lets see how we drew this Preview Badge.

The Badge consists of only 2 components:

  1. Rounded Rectangle ( Background )
  2. Normal Rectangles ( LED Lights )

Let’s see how we create rounded rectangles in android. 

// Draw Background
canvas.drawRoundRect(bgBounds, 25f, 25f, bgPaint)

Using drawRoundRect() we can easily create the badge background. 25f specified is the corner radius of the rectangle.

The LED Lights are just drawable resources which are used according to the current state of the LED.

private fun drawLED(condition: Boolean, canvas: Canvas, xValue: Int, yValue: Int) {
   if (condition) {
       ledEnabled.bounds = cells[xValue].list[yValue]
       ledEnabled.draw(canvas)
   } else {
       ledDisabled.bounds = cells[xValue].list[yValue]
       ledDisabled.draw(canvas)
   }
}

This function draws the LED Lights if the condition is satisfied.

When we consider a custom view, we need to consider the changes which occur according. These layout changes are to be controlled and maintained accordingly, Let’s see how we manage the positioning of the led lights for every android device. Spoiler: Simple 10th Grade Maths xD

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
   super.onLayout(changed, left, top, right, bottom)
   val offset = 30
   val singleCell = (right - left - (offset * 3)) / badgeWidth
   val offsetXToAdd: Int = ((((right - offset).toFloat() - (left + offset).toFloat()) - (singleCell * badgeWidth)) / 2).toInt() + 1

   cells = mutableListOf()
   for (i in 0 until badgeHeight) {
       cells.add(Cell())
       for (j in 0 until badgeWidth) {
           cells[i].list.add(Rect(
               (offsetXToAdd * 2) + j * singleCell,
               (offsetXToAdd * 2) + i * singleCell,
               (offsetXToAdd * 2) + j * singleCell + singleCell,
               (offsetXToAdd * 2) + i * singleCell + singleCell
           ))
       }
   }
   bgBounds = RectF((offsetXToAdd).toFloat(), (offsetXToAdd).toFloat(), ((singleCell * badgeWidth) + (offsetXToAdd * 3)).toFloat(), ((singleCell * badgeHeight) + (offsetXToAdd * 3)).toFloat())
}

We create an offset which is nothing but the gap from the screen edge to the badge itself, now we need to have gaps on both sides of the badge and we also leave half the offset inside the badge which is the difference between the badge background and the LED starting point, hence we calculate the value of single cells by: 

val singleCell = (right - left - (offset * 3)) / badgeWidth

We minus the no of pixels on the right of the display to the left, to get the width of the actual screen. Then we minus the padding from the left and right which is offset * 3 . Now we divide it by the number of cells we want in the badge which is the badgeWidth.

Once we have the number of cells, we want to calculate the left, right, top and bottom positions of all the LED. What we now do is loop into the number of LEDs and then multiply the singleLed width with the current position to get the accurate pixels which need to be escaped from the left.

cells = mutableListOf()
   for (i in 0 until badgeHeight) {
       cells.add(Cell())
       for (j in 0 until badgeWidth) {
           cells[i].list.add(Rect(
               (offsetXToAdd * 2) + j * singleCell,
               (offsetXToAdd * 2) + i * singleCell,
               (offsetXToAdd * 2) + j * singleCell + singleCell,
               (offsetXToAdd * 2) + i * singleCell + singleCell
           ))
       }
   }

Now the fun part, we save all of it in a 2D ArrayList to be able to draw it later on.

Conclusion

Working on custom views is very unique. This experience is one of a kind and drawing stuff with basic maths is fun in the first place. Simple equations led me to create a preview which simulates the complete badge in software. 

Ressources

Continue Reading Creating Custom Widgets in Badge Magic Android

Create an App Widget for Bookmarked Sessions for the Open Event Android App

What is an app widget?

App Widgets are miniature application views that can be embedded in other applications (such as the Home screen) and receive periodic updates. These views are referred to as Widgets in the user interface, and you can publish one with an App Widget provider. – (Android Documentation).

Android widget is an important functionality that any app can take advantage of. It could be used to show important dates, things that the user personalizes on the app etc. In the context of the Open Event Android App, it was necessary to create a bookmark widget for the Android phones so that the user could see his bookmarks on the homescreen itself and need not open the app for the same. In the open event android app, the widget was already created but it needed bug fixes and UI enhancements due to migration to the Realm database migration. Therefore, my majority of work circled around that.

Implementation

Declare the app widget in the manifest. All the updates in the application would be received by the class which extends the AppWidgetProvider if it needs to be reflected in the widget.

<receiver
   android:name=".widget.BookmarkWidgetProvider"
   android:enabled="true"
   android:label="Bookmarks">
   <intent-filter>
       <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
       <action android:name="${applicationId}.ACTION_DATA_UPDATED" />
       <action android:name="${applicationId}.UPDATE_MY_WIDGET" />
   </intent-filter>
   <meta-data
       android:name="android.appwidget.provider"
       android:resource="@xml/widget_info" />
</receiver>

 

Create a layout for the widget that is to be displayed on the homescreen. Remember to use only the views defined in the documentation. After the creation of the layout, create a custom widget updater which will broadcast the data from the app to the receiver to update the widget.

public class WidgetUpdater {
   public  static  void updateWidget(Context context){
       int widgetIds[] = AppWidgetManager.getInstance(context.getApplicationContext()).getAppWidgetIds(new ComponentName(context.getApplicationContext(), BookmarkWidgetProvider.class));
       BookmarkWidgetProvider bookmarkWidgetProvider = new BookmarkWidgetProvider();
       bookmarkWidgetProvider.onUpdate(context.getApplicationContext(), AppWidgetManager.getInstance(context.getApplicationContext()),widgetIds);
       context.sendBroadcast(new Intent(BookmarkWidgetProvider.ACTION_UPDATE));
   }
}

 

Next, create a custom RemoteViewService to update the views in the widget. The reason this is required is because the app widget does not operate in the usual lifecycle of the app. And therefore a remote service is required which acts as the remote adapter to connect to the remote views. In your class, override the onGetViewFactory() method and create a new remoteViewsFactory object to get the the data from the app on updation of the bookmark list. To populate the remote views, override the getViewsAt() method.

public class BookmarkWidgetRemoteViewsService extends RemoteViewsService {

@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {

return new RemoteViewsFactory() {
   private MatrixCursor data = null;

   @Override
   public void onCreate() {
       //Called when your factory is first constructed.
   }

   @Override
   public void onDataSetChanged() {
       }

   @Override
   public RemoteViews getViewAt(int position) {
       } 
   }
}

 

Finally, create a custom AppWidgetProvider which parses the relevant fields out of the intent and updates the UI. It acts like a broadcast receiver, hence all the updates by the widgetUpdater is received here.

public class BookmarkWidgetProvider extends AppWidgetProvider {

   public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
	  RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.bookmark_widget);
             setRemoteAdapter(context, views);

   }

   @Override
   public void onReceive(@NonNull Context context, @NonNull Intent intent) {
       super.onReceive(context, intent);
   }

   private void setRemoteAdapter(Context context, @NonNull final RemoteViews views) {
       views.setRemoteAdapter(R.id.widget_list,
               new Intent(context, BookmarkWidgetRemoteViewsService.class));
   }

}

 

Conclusion

For any event based apps, it is crucial that it regularly provide updates to its users and therefore app widget forms an integral part of that whole experience.

References

 

 

Continue Reading Create an App Widget for Bookmarked Sessions for the Open Event Android App