Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

  • Any code is not a possibility, I know it's possible to solve the problem with: second.setVisibility(second.getHeight() < SECONDS_PREFERRED_SIZE? GONE : VISIBLE) but I don't have access to getHeight() so the solution must be bare xml.
    It can be relaxed to code only writing UI state via RemoteViews.
  • Based on the above no custom views are allowed, not even libraries, just basic layout managers:
    FrameLayout, LinearLayout, RelativeLayout, GridLayout
    or as a last resort one of the advanced ones:
    ViewFlipper, ListView, GridView, StackView or AdapterViewFlipper
  • The layout_width and layout_height is dynamic
    In the example I fixed it so it's easy to try out different variations. The above image is the layout_height changed to the displayed value simulating the possibilities I want to handle.
  • I used TextView, but it should be generally applicable to any View
  • Observant people may have noticed that the above constrains the solution to Home screen app widgets with RemoteViews.

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>
like image 374
TWiStErRob Avatar asked Nov 07 '14 13:11

TWiStErRob


2 Answers

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.

like image 120
Andrew Orobator Avatar answered Oct 17 '22 09:10

Andrew Orobator


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);
}
like image 20
TWiStErRob Avatar answered Oct 17 '22 09:10

TWiStErRob