Integrate Detail widget, showing a full list of weather information and allowing clicking through to individual days

This commit is contained in:
Dan Galpin
2015-05-25 05:54:56 -07:00
parent 5628b0b2a4
commit 52efa534a2
16 changed files with 535 additions and 28 deletions

View File

@@ -122,6 +122,23 @@
android:resource="@xml/widget_info_today" />
</receiver>
<service android:name=".widget.TodayWidgetIntentService" />
<receiver
android:name=".widget.DetailWidgetProvider"
android:label="@string/title_widget_detail"
android:enabled="@bool/widget_detail_enabled" >
<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/widget_info_detail" />
</receiver>
<service
android:name=".widget.DetailWidgetRemoteViewsService"
android:enabled="@bool/widget_detail_enabled"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
</application>
</manifest>

View File

@@ -127,13 +127,16 @@ public class ForecastAdapter extends RecyclerView.Adapter<ForecastAdapter.Foreca
mCursor.moveToPosition(position);
int weatherId = mCursor.getInt(ForecastFragment.COL_WEATHER_CONDITION_ID);
int defaultImage;
boolean useLongToday;
switch (getItemViewType(position)) {
case VIEW_TYPE_TODAY:
defaultImage = Utility.getArtResourceForWeatherCondition(weatherId);
useLongToday = true;
break;
default:
defaultImage = Utility.getIconResourceForWeatherCondition(weatherId);
useLongToday = false;
}
if ( Utility.usingLocalGraphics(mContext) ) {
@@ -154,7 +157,7 @@ public class ForecastAdapter extends RecyclerView.Adapter<ForecastAdapter.Foreca
long dateInMillis = mCursor.getLong(ForecastFragment.COL_WEATHER_DATE);
// Find TextView and set formatted date on it
forecastAdapterViewHolder.mDateView.setText(Utility.getFriendlyDayString(mContext, dateInMillis));
forecastAdapterViewHolder.mDateView.setText(Utility.getFriendlyDayString(mContext, dateInMillis, useLongToday));
// Read weather forecast from cursor
String description = Utility.getStringForWeatherCondition(mContext, weatherId);

View File

@@ -26,7 +26,6 @@ import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
@@ -55,12 +54,11 @@ import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
public class ForecastFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>, SharedPreferences.OnSharedPreferenceChangeListener {
public static final String LOG_TAG = ForecastFragment.class.getSimpleName();
private ForecastAdapter mForecastAdapter;
private RecyclerView mRecyclerView;
private int mPosition = RecyclerView.NO_POSITION;
private boolean mUseTodayLayout, mAutoSelectView;
private int mChoiceMode;
private boolean mHoldForTransition;
private long mInitialSelectedDate = -1;
private static final String SELECTED_KEY = "selected_position";
@@ -196,7 +194,6 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
locationSetting, date),
vh
);
mPosition = vh.getAdapterPosition();
}
}, emptyView, mChoiceMode);
@@ -246,11 +243,6 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
// or magically appeared to take advantage of room, but data or place in the app was never
// actually *lost*.
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(SELECTED_KEY)) {
// The Recycler View probably hasn't even been populated yet. Actually perform the
// swapout in onLoadFinished.
mPosition = savedInstanceState.getInt(SELECTED_KEY);
}
mForecastAdapter.onRestoreInstanceState(savedInstanceState);
}
@@ -304,11 +296,6 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
@Override
public void onSaveInstanceState(Bundle outState) {
// When tablets rotate, the currently selected list item needs to be saved.
// When no item is selected, mPosition will be set to RecyclerView.NO_POSITION,
// so check for that before storing.
if (mPosition != RecyclerView.NO_POSITION) {
outState.putInt(SELECTED_KEY, mPosition);
}
mForecastAdapter.onSaveInstanceState(outState);
super.onSaveInstanceState(outState);
}
@@ -340,11 +327,6 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mForecastAdapter.swapCursor(data);
if (mPosition != RecyclerView.NO_POSITION) {
// If we don't need to restart the loader, and there's a desired position to restore
// to, do so now.
mRecyclerView.smoothScrollToPosition(mPosition);
}
updateEmptyView();
if ( data.getCount() == 0 ) {
getActivity().supportStartPostponedEnterTransition();
@@ -356,11 +338,27 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
// we see Children.
if (mRecyclerView.getChildCount() > 0) {
mRecyclerView.getViewTreeObserver().removeOnPreDrawListener(this);
int itemPosition = mForecastAdapter.getSelectedItemPosition();
if ( RecyclerView.NO_POSITION == itemPosition ) itemPosition = 0;
RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(itemPosition);
if ( null != vh && mAutoSelectView ) {
mForecastAdapter.selectView( vh );
int position = mForecastAdapter.getSelectedItemPosition();
if (position == RecyclerView.NO_POSITION &&
-1 != mInitialSelectedDate) {
Cursor data = mForecastAdapter.getCursor();
int count = data.getCount();
int dateColumn = data.getColumnIndex(WeatherContract.WeatherEntry.COLUMN_DATE);
for ( int i = 0; i < count; i++ ) {
data.moveToPosition(i);
if ( data.getLong(dateColumn) == mInitialSelectedDate ) {
position = i;
break;
}
}
}
if (position == RecyclerView.NO_POSITION) position = 0;
// If we don't need to restart the loader, and there's a desired position to restore
// to, do so now.
mRecyclerView.smoothScrollToPosition(position);
RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(position);
if (null != vh && mAutoSelectView) {
mForecastAdapter.selectView(vh);
}
if ( mHoldForTransition ) {
getActivity().supportStartPostponedEnterTransition();
@@ -396,6 +394,10 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
}
}
public void setInitialSelectedDate(long initialSelectedDate) {
mInitialSelectedDate = initialSelectedDate;
}
/*
Updates the empty list view with contextually relevant information that the user can
use to determine why they aren't seeing weather.

View File

@@ -34,6 +34,7 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import com.example.android.sunshine.app.data.WeatherContract;
import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
@@ -63,6 +64,7 @@ public class MainActivity extends AppCompatActivity implements ForecastFragment.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mLocation = Utility.getPreferredLocation(this);
Uri contentUri = getIntent() != null ? getIntent().getData() : null;
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
@@ -78,8 +80,14 @@ public class MainActivity extends AppCompatActivity implements ForecastFragment.
// adding or replacing the detail fragment using a
// fragment transaction.
if (savedInstanceState == null) {
DetailFragment fragment = new DetailFragment();
if (contentUri != null) {
Bundle args = new Bundle();
args.putParcelable(DetailFragment.DETAIL_URI, contentUri);
fragment.setArguments(args);
}
getSupportFragmentManager().beginTransaction()
.replace(R.id.weather_detail_container, new DetailFragment(), DETAILFRAGMENT_TAG)
.replace(R.id.weather_detail_container, fragment, DETAILFRAGMENT_TAG)
.commit();
}
} else {
@@ -90,6 +98,10 @@ public class MainActivity extends AppCompatActivity implements ForecastFragment.
ForecastFragment forecastFragment = ((ForecastFragment)getSupportFragmentManager()
.findFragmentById(R.id.fragment_forecast));
forecastFragment.setUseTodayLayout(!mTwoPane);
if (contentUri != null) {
forecastFragment.setInitialSelectedDate(
WeatherContract.WeatherEntry.getDateFromUri(contentUri));
}
SunshineSyncAdapter.initializeSyncAdapter(this);

View File

@@ -72,7 +72,7 @@ public class Utility {
* @param dateInMillis The date in milliseconds
* @return a user-friendly representation of the date.
*/
public static String getFriendlyDayString(Context context, long dateInMillis) {
public static String getFriendlyDayString(Context context, long dateInMillis, boolean displayLongToday) {
// The day string for forecast uses the following logic:
// For today: "Today, June 8"
// For tomorrow: "Tomorrow"
@@ -87,7 +87,7 @@ public class Utility {
// If the date we're building the String for is today's date, the format
// is "Today, June 24"
if (julianDay == currentJulianDay) {
if (displayLongToday && julianDay == currentJulianDay) {
String today = context.getString(R.string.today);
int formatId = R.string.format_full_friendly_date;
return String.format(context.getString(

View File

@@ -0,0 +1,89 @@
package com.example.android.sunshine.app.widget;
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.app.TaskStackBuilder;
import android.widget.RemoteViews;
import com.example.android.sunshine.app.DetailActivity;
import com.example.android.sunshine.app.MainActivity;
import com.example.android.sunshine.app.R;
import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
/**
* Provider for a scrollable weather detail widget
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class DetailWidgetProvider extends AppWidgetProvider {
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// Perform this loop procedure for each App Widget that belongs to this provider
for (int appWidgetId : appWidgetIds) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_detail);
// Create an Intent to launch MainActivity
Intent intent = new Intent(context, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.widget, pendingIntent);
// Set up the collection
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
setRemoteAdapter(context, views);
} else {
setRemoteAdapterV11(context, views);
}
boolean useDetailActivity = context.getResources()
.getBoolean(R.bool.use_detail_activity);
Intent clickIntentTemplate = useDetailActivity
? new Intent(context, DetailActivity.class)
: new Intent(context, MainActivity.class);
PendingIntent clickPendingIntentTemplate = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(clickIntentTemplate)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
views.setPendingIntentTemplate(R.id.widget_list, clickPendingIntentTemplate);
views.setEmptyView(R.id.widget_list, R.id.widget_empty);
// Tell the AppWidgetManager to perform an update on the current app widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
@Override
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
super.onReceive(context, intent);
if (SunshineSyncAdapter.ACTION_DATA_UPDATED.equals(intent.getAction())) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(
new ComponentName(context, getClass()));
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
}
}
/**
* Sets the remote adapter used to fill in the list items
*
* @param views RemoteViews to set the RemoteAdapter
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
private void setRemoteAdapter(Context context, @NonNull final RemoteViews views) {
views.setRemoteAdapter(R.id.widget_list,
new Intent(context, DetailWidgetRemoteViewsService.class));
}
/**
* Sets the remote adapter used to fill in the list items
*
* @param views RemoteViews to set the RemoteAdapter
*/
@SuppressWarnings("deprecation")
private void setRemoteAdapterV11(Context context, @NonNull final RemoteViews views) {
views.setRemoteAdapter(0, R.id.widget_list,
new Intent(context, DetailWidgetRemoteViewsService.class));
}
}

View File

@@ -0,0 +1,175 @@
package com.example.android.sunshine.app.widget;
import android.annotation.TargetApi;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.util.Log;
import android.widget.AdapterView;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.Target;
import com.example.android.sunshine.app.R;
import com.example.android.sunshine.app.Utility;
import com.example.android.sunshine.app.data.WeatherContract;
import java.util.concurrent.ExecutionException;
/**
* RemoteViewsService controlling the data being shown in the scrollable weather detail widget
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class DetailWidgetRemoteViewsService extends RemoteViewsService {
public final String LOG_TAG = DetailWidgetRemoteViewsService.class.getSimpleName();
private static final String[] FORECAST_COLUMNS = {
WeatherContract.WeatherEntry.TABLE_NAME + "." + WeatherContract.WeatherEntry._ID,
WeatherContract.WeatherEntry.COLUMN_DATE,
WeatherContract.WeatherEntry.COLUMN_WEATHER_ID,
WeatherContract.WeatherEntry.COLUMN_SHORT_DESC,
WeatherContract.WeatherEntry.COLUMN_MAX_TEMP,
WeatherContract.WeatherEntry.COLUMN_MIN_TEMP
};
// these indices must match the projection
static final int INDEX_WEATHER_ID = 0;
static final int INDEX_WEATHER_DATE = 1;
static final int INDEX_WEATHER_CONDITION_ID = 2;
static final int INDEX_WEATHER_DESC = 3;
static final int INDEX_WEATHER_MAX_TEMP = 4;
static final int INDEX_WEATHER_MIN_TEMP = 5;
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new RemoteViewsFactory() {
private Cursor data = null;
@Override
public void onCreate() {
// Nothing to do
}
@Override
public void onDataSetChanged() {
if (data != null) {
data.close();
}
// This method is called by the app hosting the widget (e.g., the launcher)
// However, our ContentProvider is not exported so it doesn't have access to the
// data. Therefore we need to clear (and finally restore) the calling identity so
// that calls use our process and permission
final long identityToken = Binder.clearCallingIdentity();
String location = Utility.getPreferredLocation(DetailWidgetRemoteViewsService.this);
Uri weatherForLocationUri = WeatherContract.WeatherEntry
.buildWeatherLocationWithStartDate(location, System.currentTimeMillis());
data = getContentResolver().query(weatherForLocationUri,
FORECAST_COLUMNS,
null,
null,
WeatherContract.WeatherEntry.COLUMN_DATE + " ASC");
Binder.restoreCallingIdentity(identityToken);
}
@Override
public void onDestroy() {
if (data != null) {
data.close();
data = null;
}
}
@Override
public int getCount() {
return data == null ? 0 : data.getCount();
}
@Override
public RemoteViews getViewAt(int position) {
if (position == AdapterView.INVALID_POSITION ||
data == null || !data.moveToPosition(position)) {
return null;
}
RemoteViews views = new RemoteViews(getPackageName(),
R.layout.widget_detail_list_item);
int weatherId = data.getInt(INDEX_WEATHER_CONDITION_ID);
int weatherArtResourceId = Utility.getIconResourceForWeatherCondition(weatherId);
Bitmap weatherArtImage = null;
if ( !Utility.usingLocalGraphics(DetailWidgetRemoteViewsService.this) ) {
String weatherArtResourceUrl = Utility.getArtUrlForWeatherCondition(
DetailWidgetRemoteViewsService.this, weatherId);
try {
weatherArtImage = Glide.with(DetailWidgetRemoteViewsService.this)
.load(weatherArtResourceUrl)
.asBitmap()
.error(weatherArtResourceId)
.into(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).get();
} catch (InterruptedException | ExecutionException e) {
Log.e(LOG_TAG, "Error retrieving large icon from " + weatherArtResourceUrl, e);
}
}
String description = data.getString(INDEX_WEATHER_DESC);
long dateInMillis = data.getLong(INDEX_WEATHER_DATE);
String formattedDate = Utility.getFriendlyDayString(
DetailWidgetRemoteViewsService.this, dateInMillis, false);
double maxTemp = data.getDouble(INDEX_WEATHER_MAX_TEMP);
double minTemp = data.getDouble(INDEX_WEATHER_MIN_TEMP);
String formattedMaxTemperature =
Utility.formatTemperature(DetailWidgetRemoteViewsService.this, maxTemp);
String formattedMinTemperature =
Utility.formatTemperature(DetailWidgetRemoteViewsService.this, minTemp);
if (weatherArtImage != null) {
views.setImageViewBitmap(R.id.widget_icon, weatherArtImage);
} else {
views.setImageViewResource(R.id.widget_icon, weatherArtResourceId);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
setRemoteContentDescription(views, description);
}
views.setTextViewText(R.id.widget_date, formattedDate);
views.setTextViewText(R.id.widget_description, description);
views.setTextViewText(R.id.widget_high_temperature, formattedMaxTemperature);
views.setTextViewText(R.id.widget_low_temperature, formattedMinTemperature);
final Intent fillInIntent = new Intent();
String locationSetting =
Utility.getPreferredLocation(DetailWidgetRemoteViewsService.this);
Uri weatherUri = WeatherContract.WeatherEntry.buildWeatherLocationWithDate(
locationSetting,
dateInMillis);
fillInIntent.setData(weatherUri);
views.setOnClickFillInIntent(R.id.widget_list_item, fillInIntent);
return views;
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
private void setRemoteContentDescription(RemoteViews views, String description) {
views.setContentDescription(R.id.widget_icon, description);
}
@Override
public RemoteViews getLoadingView() {
return new RemoteViews(getPackageName(), R.layout.widget_detail_list_item);
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public long getItemId(int position) {
if (data.moveToPosition(position))
return data.getLong(INDEX_WEATHER_ID);
return position;
}
@Override
public boolean hasStableIds() {
return true;
}
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/widget_margin">
<FrameLayout
android:id="@+id/widget"
android:layout_width="match_parent"
android:layout_height="@dimen/abc_action_bar_default_height_material"
android:background="@color/primary">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/app_name"
android:layout_gravity="center"
android:src="@drawable/ic_logo"
/>
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@color/primary_light">
<ListView
android:id="@+id/widget_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@null"
android:dividerHeight="0dp"
tools:listitem="@layout/widget_detail_list_item"/>
<TextView
android:id="@+id/widget_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:fontFamily="sans-serif-condensed"
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/empty_forecast_list"/>
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Layout for weather forecast list item for future day (not today) -->
<!-- Make the background of our selector a non-transparent color -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_list_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/touch_selector_white"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingLeft="@dimen/abc_list_item_padding_horizontal_material"
android:paddingRight="@dimen/abc_list_item_padding_horizontal_material"
android:orientation="horizontal">
<ImageView
android:id="@+id/widget_icon"
android:layout_gravity="center"
android:layout_width="@dimen/list_icon"
android:layout_height="@dimen/list_icon"
android:layout_marginRight="@dimen/abc_list_item_padding_horizontal_material"
android:layout_marginEnd="@dimen/abc_list_item_padding_horizontal_material"
tools:src="@drawable/ic_clear"
/>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="7"
android:orientation="vertical">
<TextView
android:id="@+id/widget_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textColor="@color/primary_text"
android:text="Today, May 21"/>
<TextView
android:id="@+id/widget_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="@color/secondary_text"
tools:text="@string/condition_500"/>
</LinearLayout>
<TextView
android:id="@+id/widget_high_temperature"
android:layout_width="@dimen/forecast_widget_text_width"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="right"
android:layout_marginRight="@dimen/forecast_temperature_space"
android:layout_marginEnd="@dimen/forecast_temperature_space"
android:fontFamily="sans-serif-light"
android:textColor="@color/primary_text"
android:textSize="@dimen/forecast_widget_text_size"
tools:text="10"/>
<TextView
android:id="@+id/widget_low_temperature"
android:layout_width="@dimen/forecast_widget_text_width"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="right"
android:fontFamily="sans-serif-light"
android:textColor="@color/forecast_low_text"
android:textSize="@dimen/forecast_widget_text_size"
tools:text="7"/>
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="use_detail_activity">false</bool>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="widget_detail_enabled">true</bool>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="widget_detail_enabled">false</bool>
<bool name="use_detail_activity">true</bool>
</resources>

View File

@@ -59,4 +59,19 @@
<dimen name="widget_today_min_resize_width">40dp</dimen>
<dimen name="widget_today_min_resize_height">@dimen/widget_today_default_height</dimen>
<dimen name="widget_today_large_width">220dp</dimen>
<dimen name="widget_detail_default_width">250dp</dimen>
<dimen name="widget_detail_default_height">180dp</dimen>
<dimen name="widget_detail_min_resize_width">220dp</dimen>
<dimen name="widget_detail_min_resize_height">@dimen/widget_detail_default_height</dimen>
<!-- Text Sizes - We are using DP here rather than SP because these are already large
font sizes, and going larger will cause lots of view problems. This is only for
the large forecast numbers in the forecast list -->
<dimen name="forecast_widget_text_size">24dp</dimen>
<!-- This is an odd width, but we're trying to match the font closely to keep things working
on devices that don't yet have Roboto -->
<dimen name="forecast_widget_text_width">38dp</dimen>
</resources>

View File

@@ -138,6 +138,7 @@
<!-- Strings related to Widgets -->
<string name="title_widget_today">Sunshine Today</string>
<string name="title_widget_detail">Sunshine Details</string>
<!-- Empty Weather Database -->
<string name="empty_forecast_list">No Weather Information Available</string>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:initialKeyguardLayout="@layout/widget_detail"
android:initialLayout="@layout/widget_detail"
android:minHeight="@dimen/widget_detail_default_height"
android:minResizeHeight="@dimen/widget_detail_min_resize_height"
android:minResizeWidth="@dimen/widget_detail_min_resize_width"
android:minWidth="@dimen/widget_detail_default_width"
android:previewImage="@drawable/widget_preview_detail"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="0"
android:widgetCategory="home_screen|keyguard"
tools:ignore="UnusedAttribute" />