Reputation: 89
I'm trying to make a widget clock.
On my MainActivity, the clock is displayed perfectly using runOnUiThread.
Since the widget doesn't extend "activity" I cannot runOnUiThread.
But when I try to use Thread with the widget I get errors (or) a blank widget.
public class NewAppWidget extends AppWidgetProvider {
static Context cont;
static AppWidgetManager awm;
static int awid;
static Thread t;
static RemoteViews views;
static String timeText;
static String dateText;
public static Bitmap BuildUpdate(String txttime, int size,Context context){
Paint paint= new Paint();
paint.setTextSize(size);
Typeface
custTface=Typeface.createFromAsset(context.getAssets(),"fonts/Lato-Regular.ttf");
paint.setTypeface(custTface);
paint.setColor(Color.WHITE);
paint.setTextAlign(Paint.Align.LEFT);
paint.setSubpixelText(true);
paint.setAntiAlias(true);
// - in the next line is highly important.
float baseline = -paint.ascent();
int width =(int) paint.measureText(txttime+0.5f);
int height= (int) (baseline+paint.descent()+0.5f);
Bitmap image = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
Canvas canvas=new Canvas(image);
canvas.drawText(txttime,0,baseline,paint);
return image;
}
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
cont=context;
awm=appWidgetManager;
awid=appWidgetId;
t = new Thread() {
@Override
public void run() {
while (!isInterrupted()){
try {
t.sleep(1000);
new Thread(new Runnable(){
@Override
public void run() {
long date = System.currentTimeMillis();
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
SimpleDateFormat sdf2 = new SimpleDateFormat("hh-mm-ss a");
timeText = sdf2.format(date);
dateText = sdf.format(date);
views.setImageViewBitmap(R.id.txtTime, BuildUpdate(timeText, 100, cont));
views.setImageViewBitmap(R.id.txtDate, BuildUpdate(dateText, 25, cont));
awm.updateAppWidget(awid, views);
}
});
}catch (InterruptedException e) {
}
}
}
};
t.start();
}
This produces a blank widget.
I'm not asking for the code, just help me identify what is my mistake here & How should I approach that?
Upvotes: 2
Views: 1510
Reputation: 445
I know that this is an old problem and it has been resolved for author of this thread.
However for future readers I would like to modify answer given by #oaskamay (https://stackoverflow.com/a/49520145/13755907).
The second approach, using Handler, worked for the clock problem. This is correct and reasonable in this case.
The first approach is of no use. As this is AppWidgetProvider class called by the system (when new Widget is created on the screen) only default constructor is used, thus there is no way of providing Activity during creating a new instance
However if anyone still needs to use non-UI Thread in the widget, like for fetching data for internet, which Android prohibits to be run on any UI Thread, then solution to above would be to use... Handler but within Thread. This way you will return with your data from non-UI Worker to UI based Handler. I marked new pieces of the code with // New line comment. I also changed the method from static to standard (I no not understand the purpose of static methods here).
public class NewAppWidget extends AppWidgetProvider {
private Context cont;
private AppWidgetManager awm;
private int awid;
private Thread t;
private RemoteViews views;
private String timeText;
private String dateText;
// New line
private Handler handler = new Handler(Looper.getMainLooper());
public Bitmap BuildUpdate(String txttime, int size,Context context){
Paint paint= new Paint();
paint.setTextSize(size);
Typeface
custTface=Typeface.createFromAsset(context.getAssets(),"fonts/Lato-Regular.ttf");
paint.setTypeface(custTface);
paint.setColor(Color.WHITE);
paint.setTextAlign(Paint.Align.LEFT);
paint.setSubpixelText(true);
paint.setAntiAlias(true);
// - in the next line is highly important.
float baseline = -paint.ascent();
int width =(int) paint.measureText(txttime+0.5f);
int height= (int) (baseline+paint.descent()+0.5f);
Bitmap image = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
Canvas canvas=new Canvas(image);
canvas.drawText(txttime,0,baseline,paint);
return image;
}
void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
cont=context;
awm=appWidgetManager;
awid=appWidgetId;
t = new Thread() {
@Override
public void run() {
while (!isInterrupted()){
try {
t.sleep(1000);
new Thread(new Runnable(){
@Override
public void run() {
long date = System.currentTimeMillis();
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
SimpleDateFormat sdf2 = new SimpleDateFormat("hh-mm-ss a");
timeText = sdf2.format(date);
dateText = sdf.format(date);
// New line
handler.post(new Runnable() {
@Override
public void run() { views.setImageViewBitmap(R.id.txtTime, BuildUpdate(timeText, 100, cont));
views.setImageViewBitmap(R.id.txtDate, BuildUpdate(dateText, 25, cont));
awm.updateAppWidget(awid, views);
}
});
}
});
}catch (InterruptedException e) {
}
}
}
};
t.start();
}
}
Upvotes: 0
Reputation: 274
Your Thread
approach does not work because Android disallows Thread
s who did not create the View
hierarchy from touching those View
s. Like most cases, your View
that you're trying to update from your worker Thread
was most likely inflated and instantiated on the UI thread as part of your Activity
's creation process by Android.
I think you have two options here:
Always pass an Activity
as your Context
argument in your NewAppWidget
constructor.
Then you can either safely cast the Context
object to an Activity
and call Activity.runOnUiThread(Runnable)
, or change the Context
field to be an Activity
to avoid having to cast every time.
Use a Handler
.
Instantiate your Handler
as follows. This will create a Handler
object that is linked to the UI thread (also called the "main" thread).
Handler handler = new Handler(Looper.getMainLooper());
You can then use the Handler
to post Runnable
s which will then be run on the UI Thread
from your worker Thread
. You'll be able to update View
s from your worker Thread
this way:
handler.post(new Runnable() { ... });
I recommend the second approach as it is cleaner and you won't always need a reference to an Activity
this way.
Keep in mind with the second approach, in your Runnable
you'll want to use a WeakReference
to your Activity
or View
that you want to update to prevent memory leaks. Your worker Thread
may still be running even when your Activity
or View
is (supposed to be) garbage collected. Using WeakReference
allows the garbage collector to collect your Activity
/ View
even when your worker Thread
is still running.
Upvotes: 1