2016年3月1日 星期二

Create widget(With list view) with remote view service and remote view Factory

Create widget with remote view service and remote view Factory
the remote View factor is a cursor adapter for remote view


create the click

now start
step1 create a Detail widget Layout


detail_widget.xml


example code:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:padding="@dimen/widget_margin">


   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">


       <LinearLayout
           android:layout_width="match_parent"
           android:id="@+id/detail_widget_bar"
           android:layout_height="@dimen/detail_widget_bar_height"
           android:background="@color/primary_light"
           android:orientation="horizontal">


           <ImageView
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:src="@drawable/ic_clear"/>


           <ImageView
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:src="@drawable/ic_logo"/>
       </LinearLayout>


       <ListView
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:id="@+id/detail_widget_list" />


   </LinearLayout>


</FrameLayout>

detail_widget_list_iteam.xml


example code:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:padding="@dimen/widget_margin">


   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">


       <LinearLayout
           android:layout_width="match_parent"
           android:id="@+id/detail_widget_bar"
           android:layout_height="@dimen/detail_widget_bar_height"
           android:background="@color/primary_light"
           android:orientation="horizontal">


           <ImageView
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:src="@drawable/ic_clear"/>


           <ImageView
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:src="@drawable/ic_logo"/>
       </LinearLayout>


       <ListView
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:id="@+id/detail_widget_list" />


   </LinearLayout>


</FrameLayout>




step2 AppwidgetProvider info


example code:


<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   android:initialKeyguardLayout="@layout/detail_widget"
   android:initialLayout="@layout/detail_widget"
   android:minHeight="250dp"
   android:minWidth="180dp"
   android:resizeMode="vertical"
   android:updatePeriodMillis="0"
   android:widgetCategory="home_screen"/>


step3 Appwidget provioder


public class DetailWidget extends AppWidgetProvider {


   @Override
   public void onReceive(Context context, Intent intent) {
       super.onReceive(context, intent);
       if (SunshineSyncAdapter.ACTION_DATA_UPDATED.equals(intent.getAction())) {
           Intent startintent = new Intent(context, DetailWidgetIntentService.class);
           context.startService(startintent);
       }
   }


   @Override
   public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
       Intent intent = new Intent(context, DetailWidgetIntentService.class);
       context.startService(intent);


   }


   @Override
   public void onEnabled(Context context) {
       // Enter relevant functionality for when the first widget is created
   }


   @Override
   public void onDisabled(Context context) {
       // Enter relevant functionality for when the last widget is disabled
   }
}


DetailWidgetIntentService

public class DetailWidgetIntentService extends IntentService {


   /**
    * Creates an IntentService.  Invoked by your subclass's constructor.
    *
    * @param name Used to name the worker thread, important only for debugging.
    */
   public DetailWidgetIntentService(String name) {
       super(name);
   }


   static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                               int appWidgetId) {

       // Construct the RemoteViews object
       RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.detail_widget);

       // Instruct the widget manager to update the widget
       appWidgetManager.updateAppWidget(appWidgetId, views);
   }


   @Override
   protected void onHandleIntent(Intent intent) {
       Context context=this;
       AppWidgetManager appWidgetManager=AppWidgetManager.getInstance(this);
       int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(this, DetailWidget.class));
       for (int appWidgetId : appWidgetIds) {
           updateAppWidget(context, appWidgetManager, appWidgetId);
       }
   }

}





step4 change the Manifest file


<receiver android:name=".widget.DetailWidget">
           <intent-filter>
               <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
               <action android:name="com.example.android.sunshine.app.ACTION_DATA_UPDATED" />
           </intent-filter>


           <meta-data
               android:name="android.appwidget.provider"
               android:resource="@xml/detail_widget_info" />
       </receiver>

step5 How to make the ListView in the widget show the data?



Normally,we use the ListView,we need the Adapter.In the widget,we use RemoteViewsFactory.
so we create a 1.class extends RemoteViewsFactory first

public class DetailWidgetListAdapter implements RemoteViewsService.RemoteViewsFactory {
   private Context context;
   public DetailWidgetListAdapter(Context context) {
       this.context=context;
   }


   private int[] data;
   @Override
   public void onCreate() {
       data=new int[]{1,2,3,4,5,6};


   }


   @Override
   public void onDataSetChanged() {


   }


   @Override
   public void onDestroy() {


   }


   @Override
   public int getCount() {
       return data.length;
   }


   @Override
   public RemoteViews getViewAt(int position) {
       RemoteViews rview=new RemoteViews(context.getPackageName(), R.layout.detail_widget_list_iteam);
       rview.setTextViewText(R.id.detail_widget_list_item_date_textview,data[position]+"");
       return rview;
   }


   @Override
   public RemoteViews getLoadingView() {
       return null;
   }


   @Override
   public int getViewTypeCount() {
       return 1;
   }


   @Override
   public long getItemId(int position) {
       return position;
   }


   @Override
   public boolean hasStableIds() {
       return false;
   }
}


the getView at cretae the remote view for the ListView iteam




how we set the RemoteViewsFactory(adapter) to the ListView in the wiget?we need RemoteViewsService first.
2.create a class implements RemoteViewsService


public class MyWidgetService extends RemoteViewsService {
   @Override
   public RemoteViewsFactory onGetViewFactory(Intent intent) {
       Log.d("DetailonUpdate","onGetViewFactory");
       return new DetailWidgetListAdapter(getApplicationContext());
   }
}

it is simple now,just return,it is not running in the UI thread,so we can do some long work at here.e.e. get data from content provider

how to 3.set a adapter to the list view?


RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.detail_widget);


//set the array adapter
           Intent intent = new Intent(context, MyWidgetService.class);
           // Add the app widget ID to the intent extras.
           intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
           intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));

           // Instantiate the RemoteViews object for the App Widget layout.


           // Set up the RemoteViews object to use a RemoteViews adapter.
           // This adapter connects
           // to a RemoteViewsService  through the specified intent.
           // This is how you populate the data.
           if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
               Log.d("DetailonUpdate", "ice");
               views.setRemoteAdapter( R.id.detail_widget_list2, intent);
//                rv.setRemoteAdapter(appWidgetId, R.id.detail_widget_list, intent);
           }
           // Instruct the widget manager to update the widget
           appWidgetManager.updateAppWidget(appWidgetId, views);

The import ant thing is we need t add 4.the promise to the WidgetService


<service
           android:name=".widget.MyWidgetService"
           android:exported="false"
           android:permission="android.permission.BIND_REMOTEVIEWS" >
       </service>

it can show 1-6 no!!!!


step 6 make the widget to how the content provider data
now,we wnat the list in the widget to show the content provider data
so,we need to use the content resolver at the RemoteViewsService(because not in the UI thread),to get the data


1.get the cursor


private static final String[] FORECAST_COLUMNS = {
           // In this case the id needs to be fully qualified with a table name, since
           // the content provider joins the location & weather tables in the background
           // (both have an _id column)
           // On the one hand, that's annoying.  On the other, you can search the weather table
           // using the location set by the user, which is only in the Location table.
           // So the convenience is worth it.
           WeatherContract.WeatherEntry.TABLE_NAME + "." + WeatherContract.WeatherEntry._ID,
           WeatherContract.WeatherEntry.COLUMN_DATE,
           WeatherContract.WeatherEntry.COLUMN_SHORT_DESC,
           WeatherContract.WeatherEntry.COLUMN_MAX_TEMP,
           WeatherContract.WeatherEntry.COLUMN_MIN_TEMP,
           WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING,
           WeatherContract.WeatherEntry.COLUMN_WEATHER_ID,
           WeatherContract.LocationEntry.COLUMN_COORD_LAT,
           WeatherContract.LocationEntry.COLUMN_COORD_LONG
   };



String sortOrder = WeatherContract.WeatherEntry.COLUMN_DATE + " ASC";
       String locationSetting = Utility.getPreferredLocation(this);
       Uri weatherForLocationUri = WeatherContract.WeatherEntry.buildWeatherLocationWithStartDate(
               locationSetting, System.currentTimeMillis());


       ContentResolver resolver = getContentResolver();
       Cursor cursor = resolver.query(weatherForLocationUri, FORECAST_COLUMNS, null, null, sortOrder);

2. give the cursor to the DetailWidgetListAdapter(RemoteViewsFactory)


in  public class DetailWidgetListAdapter implements RemoteViewsService.RemoteViewsFactory,change this:

private Context mcontext;
   private Cursor mcursor;
   private int[] data;


   public DetailWidgetListAdapter(Context context, Cursor cursor) {
       this.mcontext = context;
       this.mcursor = cursor;
   }


   public Cursor getMcursor() {
       return mcursor;
   }


also it is because when onDataSetChanged and  onDestroy we will use other or will not use the same cursor object again,so need to set the mcursor=null;


   @Override
   public void onDestroy() {
       if(mcursor!=null)
           mcursor=null;
   }

important!!!!!!however,we can not do this:


public void onDataSetChanged() {
       Log.d("DetailonUpdate","onDataSetChanged");
       if (mcursor != null)
           mcursor = null;


   }
this make the cursor input become 0,because:
RemoteViewsFactory create,RemoteViewsFactory run first




3. make the list view in the widget show the data in cursor
that mean we need to change the getViewAt method.


we nee to get the datathat we need in the cursor


/*because  private static final String[] FORECAST_COLUMNS = {
           // In this case the id needs to be fully qualified with a table name, since
           // the content provider joins the location & weather tables in the background
           // (both have an _id column)
           // On the one hand, that's annoying.  On the other, you can search the weather table
           // using the location set by the user, which is only in the Location table.
           // So the convenience is worth it.
           WeatherContract.WeatherEntry.TABLE_NAME + "." + WeatherContract.WeatherEntry._ID,
           WeatherContract.WeatherEntry.COLUMN_DATE,
           WeatherContract.WeatherEntry.COLUMN_SHORT_DESC,
           WeatherContract.WeatherEntry.COLUMN_MAX_TEMP,
           WeatherContract.WeatherEntry.COLUMN_MIN_TEMP,
           WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING,
           WeatherContract.WeatherEntry.COLUMN_WEATHER_ID,
           WeatherContract.LocationEntry.COLUMN_COORD_LAT,
           WeatherContract.LocationEntry.COLUMN_COORD_LONG
   };*/
   static final int COL_WEATHER_ID = 0;
   static final int COL_WEATHER_DATE = 1;
   static final int COL_WEATHER_DESC = 2;
   static final int COL_WEATHER_MAX_TEMP = 3;
   static final int COL_WEATHER_MIN_TEMP = 4;
   static final int COL_LOCATION_SETTING = 5;
   static final int COL_WEATHER_CONDITION_ID = 6;
   static final int COL_COORD_LAT = 7;
   static final int COL_COORD_LONG = 8;

private RemoteViews binData(RemoteViews rview, Cursor mCursor) {
       Log.d("DetailonUpdate","binData");
       long dateInMillis = mCursor.getLong(COL_WEATHER_DATE);
       double high = mCursor.getDouble(COL_WEATHER_MAX_TEMP);
       double low = mCursor.getDouble(COL_WEATHER_MIN_TEMP);
       int weatherId = mCursor.getInt(COL_WEATHER_CONDITION_ID);
       int defaultImage = Utility.getIconResourceForWeatherCondition(weatherId);


       //set the icon
       rview.setImageViewResource(R.id.detail_widget_list_item_icon, defaultImage);
       //set the date
       rview.setTextViewText(R.id.detail_widget_list_item_date_textview, Utility.getFriendlyDayString(mcontext, dateInMillis));


       //set the weather desc
       String description = Utility.getStringForWeatherCondition(mcontext, weatherId);
       rview.setTextViewText(R.id.detail_widget_list_item_forecast_textview, description);
       //set the high temp
       String highString = Utility.formatTemperature(mcontext, high);
       rview.setTextViewText(R.id.detail_widget_list_item_high_textview, highString);


       //set the low temp
       String lowString = Utility.formatTemperature(mcontext, low);
       rview.setTextViewText(R.id.detail_widget_list_item_low_textview, lowString);


       return rview;
   }


@Override
   public RemoteViews getViewAt(int position) {
       Log.d("DetailonUpdate","getViewAt");
       RemoteViews rview = new RemoteViews(mcontext.getPackageName(), R.layout.detail_widget_list_iteam);
       mcursor.moveToPosition(position);
       return binData(rview, mcursor);
   }

now,it can show the iteam




however,it can not click.

3.2 it can't update correctly when the sql database 
update:see:http://pomprogrammer.blogspot.com/2016/03/list-view-in-widgetwhen-data.html


step7 handle the click


1.we want click the bar ,go to main activity
in DetailWidget onUpdate method:


RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.detail_widget);
Intent startMainintent=new Intent(context, MainActivity.class);
           PendingIntent pendingIntent=PendingIntent.getActivity(context,0,startMainintent,0);
           views.setOnClickPendingIntent(R.id.detail_widget_bar,pendingIntent);


2.we want click the iteam ,go to the right detail
2.1先用setPendingIntentTemplate方法为collection整体的点击设置一个处理的PendingIntent


in AppWidgetProvider >onUpdate


Intent clickIntentTemple=new Intent(context, DetailActivity.class);
           PendingIntent pendingIntentTemplate= TaskStackBuilder.create(context).addNextIntentWithParentStack(clickIntentTemple).getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT);
           views.setPendingIntentTemplate(R.id.detail_widget_list2,pendingIntentTemplate);


because we want to start DetailActivity.class,so DetailActivity.class in the intent


The meaninig of PendingIntent.FLAG_UPDATE_CURRENT
這個FLAG 的意思就是:如果系統中已存在該PendingIntent對象,那麼系統將保留該PendingIntent對象,但是會使用新的Intent來更新之前PendingIntent中的Intent對像數據,例如更新Intent中的Extras。

2.2然后通过RemoteViewsFactory使用setOnClickFillInIntent为collection视图中的每一项传入一个与该项相关的Intent。该Intent会被合入处理时接收到Intent中。

in DetailWidgetListAdapter implements RemoteViewsService.RemoteViewsFactory>getViewat

//set the iteam click intent
       Intent fillintent = new Intent();
       String locationSetting = Utility.getPreferredLocation(mcontext);
       Uri contentUri = WeatherContract.WeatherEntry.buildWeatherLocationWithDate(locationSetting, dateInMillis);
       fillintent.setData(contentUri);
       rview.setOnClickFillInIntent(R.id.detail_widget_list_iteam,fillintent);

it is because we already at 2.1 set the destination activity Detail activity,so we do not need to set it again.we just set the data that we want ot send to the activity.

now! when we click the iteam,we can go the the relevent detail


nut here is a problem,because our app have tablet mode and phone mode,in tablet mode ,we do not have detail activity,we only have one page,so, have error.


ste8 solve the tablet mode problrm
we can set always go to main activity in tablet mode
how?
in normal
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <bool name="tabletmode">false</bool>
</resources>


in sw 600dp
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <bool name="tabletmode">true</bool>
</resources>

in stpe7 step 1


change to
if(isTabletmode){
               //in tablety mode
               Intent liststartMainintent=new Intent(context, MainActivity.class);
               PendingIntent listpendingIntent=PendingIntent.getActivity(context,0,liststartMainintent,0);
               views.setOnClickPendingIntent(R.id.detail_widget_list2,listpendingIntent);
           }else{
               //int phone mode
               Intent clickIntentTemple=new Intent(context, DetailActivity.class);
               PendingIntent pendingIntentTemplate= TaskStackBuilder.create(context).addNextIntentWithParentStack(clickIntentTemple).getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT);
               views.setPendingIntentTemplate(R.id.detail_widget_list2,pendingIntentTemplate);
           }


in stpe7 step 2


change to


if(!isTabletmode){
           Intent fillintent = new Intent();
           String locationSetting = Utility.getPreferredLocation(mcontext);
           Uri contentUri = WeatherContract.WeatherEntry.buildWeatherLocationWithDate(locationSetting, dateInMillis);
           fillintent.setData(contentUri);
           rview.setOnClickFillInIntent(R.id.detail_widget_list_iteam,fillintent);

       }