TWiStErRob
TWiStErRob

Reputation: 46508

How to hide partially visible views in Android layout xml without code?

Please read the question fully before answering!

Suppose I have two views:

  1. first (yellow) in the bottom
  2. second (cyan) filling the rest of the view above first

The size of the parent view is dynamic.

How can I achieve the below Expected column UI when the parent height is dynamically set to the values in the first column? Notice how the partially visible view is just hidden instead of cropped.

What I want to achieve

Constraints

Tries

I tried implementing with RelativeLayout and LinearLayout, but they both behave as the Actual column on the picture.

RelativeLayout

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="64dp" (actually fill_parent, but fix it for sake of simplicity)
    android:layout_height="fill_parent" (first column on picture)
    android:layout_gravity="center"
    android:background="#666"
    tools:ignore="HardcodedText,RtlHardcoded"
    >
    <TextView
        android:id="@+id/first"
        android:layout_width="wrap_content"
        android:layout_height="16dp"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:background="#8ff0"
        android:text="First"
        />
    <TextView
        android:id="@+id/second"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_above="@id/first"
        android:background="#80ff"
        android:text="Second"
        android:gravity="center"
        />
</RelativeLayout>

LinearLayout

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="64dp" (actually fill_parent, but fix it for sake of simplicity)
    android:layout_height="fill_parent" (first column on picture)
    android:layout_gravity="center"
    android:background="#666"
    android:orientation="vertical"
    tools:ignore="HardcodedText,RtlHardcoded"
    >
    <TextView
        android:id="@+id/second"
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#80ff"
        android:text="Second"
        android:gravity="center"
        />
    <TextView
        android:id="@+id/first"
        android:layout_width="wrap_content"
        android:layout_height="16dp"
        android:layout_gravity="right"
        android:background="#8ff0"
        android:text="First"
        />
</LinearLayout>

Upvotes: 7

Views: 2042

Answers (2)

TWiStErRob
TWiStErRob

Reputation: 46508

While the OPTION_APPWIDGET_MIN/MAX_WIDTH/HEIGHT doesn't really report the real values, I found a way around it. Note that this doesn't really solve the problem at hand, but is a rather convoluted workaround.

Source of the idea

Normal update from the widget host
26061-26061/net.twisterrob.app.debug V/TAG﹕ onReceive(Intent { act=android.appwidget.action.APPWIDGET_UPDATE flg=0x10000010 cmp=net.twisterrob.app.debug/net.twisterrob.app.AppWidgetProvider (has extras) } (Bundle[{appWidgetIds=[I@42b14090}]))  
26061-26061/net.twisterrob.app.debug V/TAG﹕ onUpdate([483])  

User tap with setOnClickPendingIntent
26061-26061/net.twisterrob.app.debug V/TAG﹕ onReceive(Intent { act=android.appwidget.action.APPWIDGET_UPDATE flg=0x10000010 cmp=net.twisterrob.app.debug/net.twisterrob.app.AppWidgetProvider bnds=[281,756][533,1071] (has extras) } (Bundle[{appWidgetIds=[I@42b14090}]))  
26061-26061/net.twisterrob.app.debug V/TAG﹕ onUpdate([483])

Investigation

When RemoteViews.setOnClickPendingIntent broadcasts the tap by the user, it'll put the bounds of the view in the Intent. As you can see in bnds=[281,756][533,1071] the views is 533-281=252 wide and 1071-756=315 tall, these are pixels and the Galaxy S4 is an XXHDPI (3x) device, so 252x315 / 3 = 84x105 which is exactly what my empirical observation was about the widget cell size.

The bounds given depends on the viewId given to setOnClickPendingIntent, so to get the cell size, one will need to attach the pending intent on root view in the layout. My widget has a "tap-to-refresh" listener which I can safely attach to the root layout. There are other views which do have PendingIntents, but those cover the root layout so their intent will be broadcast, just like regular onClickListeners.

Solution

Now, this means that we know the exact cell size, based on this it may be possible to calculate some views' size by hand (for simpler layouts). Essentially we have way to get root.getHeight(), we can "hardcode" the size of first into a R.dimen.firstHeight, and from this we can calculate whether to show first, second or none.

Or, make use of these bounds just like @AndrewOrobator suggested: inflate a layout appropriate for the size.

Requires user interaction

The only problem that this requires user interaction at least once. When the widget host sends the update there are no bounds, so the user must tap a view so we have the size. I would work around this, by forcing the user to tap on the view once (just after they installed the widget to home):

@Override
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
    if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(intent.getAction())) {
        for (int appWidgetId : getAppWidgetIds(intent)) {
            Rect storedBounds = getSizeFromPreferences(appWidgetId);
            Rect sourceBounds = intent.getSourceBounds();
            if (storedBounds == null && sourceBounds != null) {
                // no stored size, but we just received one, save it
                setSizeToPreferences(appWidgetId, sourceBounds);
                storedBounds = sourceBounds;
            }
            if (storedBounds == null) {
                // we didn't have a stored size, neither received one
                // force the user to interact with the widget once so we have a size
                RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_tap_to_refresh);
                views.setOnClickPendingIntent(R.id.root, createRefresh(context, appWidgetId));
                AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views);
            } else {
                // update as usual, and use the bounds from getSizeFromPreferences(appWidgetId)
                onUpdate(context, AppWidgetManager.getInstance(context), new int[] {appWidgetId});
            }
        }
    } else {
        super.onReceive(context, intent);
    }
}
private @NonNull int[] getAppWidgetIds(Intent intent) {
    int[] appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
    if (appWidgetIds == null) {
        appWidgetIds = new int[0];
    }
    return appWidgetIds;
}
private @NonNull PendingIntent createRefresh(Context context, int appWidgetId) {
    Intent intent = new Intent(context, MyAppWidgetProvider.class);
    intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetId);
    return PendingIntent.getBroadcast(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}

Upvotes: 1

Andrew Orobator
Andrew Orobator

Reputation: 8666

If this is a RemoteViews/widget, try using multiple layout files. Extend AppWidgetProvider and override the onAppWidgetOptionsChanged method. In this function you'll want to do the following:

Bundle options = appWidgetManager.getAppWidgetOptions(appWidgetId);
int minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT);

Then case on minHeight to determine which layout is most appropriate to inflate. This page is incredibly useful when making widgets.

Upvotes: 1

Related Questions