I'm flattening my layouts by employing the new ConstraintLayout (1.1.2) but I can no longer control the visibility of a ProgressBar when it's in a group. Here's the simplest version of my layout XML file which reproduces the issue:
<android.support.constraint.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!-- This group prevents me from hiding ProgressBar -->
<android.support.constraint.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="imageLoadingSpinner" />
<ProgressBar
android:id="#+id/imageLoadingSpinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</android.support.constraint.ConstraintLayout>
I've introduced the Group in order to control some of the layout, although for simplicity I've left all that out. The essence of the bug is introducing the group prevents me from setting the ProgressBar's visibility to GONE.
The following code no longer hides the ProgressBar:
find(R.id.imageLoadingSpinner).setVisibility(GONE);
To be clear, if I remove the group, as shown below, I can set the ProgressBar's visibility to GONE:
<android.support.constraint.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!-- Commenting out this group allows me to hide ProgressBar -->
<!--<android.support.constraint.Group-->
<!--android:layout_width="wrap_content"-->
<!--android:layout_height="wrap_content"-->
<!--app:constraint_referenced_ids="imageLoadingSpinner" />-->
<ProgressBar
android:id="#+id/imageLoadingSpinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</android.support.constraint.ConstraintLayout>
I can't find any information about this issue. ProgressBar visibility issue is unrelated.
While, as has been said, you can't change the visibility of just a single Group's child (and for that I find this widget much less helpful and fun), you can always set its alpha property.
So if you don't mess in any way with an alpha value of a view you want to hide, you can just do the following:
find(R.id.imageLoadingSpinner).setAlpha(0);
And I can't quite find a reason not to do this, except maybe the fact that the view will probably still be rendered and then all the rendered pixels will be converted to 0-alpha and thus will become invisible wasting a few clock's cycles, but in most cases it would be an exaggeration to consider this as a problem.
I don't think it's an issue because essentially that's the use of the Group, according to the docs:
This class controls the visibility of a set of referenced widgets.
Also:
The visibility of the group will be applied to the referenced widgets. It's a convenient way to easily hide/show a set of widgets without having to maintain this set programmatically.
So you have to set the visibility on the group itself. I don't know what you use the group for, because you didn't specify, but maybe you should restructure to better take advantage of it, or get rid of it completely.
Just to add to the variety of solutions - you could extend Group and make it behave like you would expect. It's a bit more convenient if you use groups a lot and you expect normal parent-child behaviour in terms of visibility.
public class LayoutGroup extends Group {
public LayoutGroup(Context context) {
super(context);
}
public LayoutGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LayoutGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public void updatePreLayout(ConstraintLayout container) {
int visibility = this.getVisibility();
float elevation = 0.0F;
if (Build.VERSION.SDK_INT >= 21) {
elevation = this.getElevation();
}
for(int i = 0; i < this.mCount; ++i) {
int id = this.mIds[i];
View view = container.getViewById(id);
if (view != null) {
// If the group is visible, let the child control visibility
view.setVisibility(visibility == VISIBLE ? view.getVisibility() : visibility);
if (elevation > 0.0F && Build.VERSION.SDK_INT >= 21) {
view.setElevation(elevation);
}
}
}
}
}
For reference, here is the code from the base class (Group):
for(int i = 0; i < this.mCount; ++i) {
int id = this.mIds[i];
View view = container.getViewById(id);
if (view != null) {
view.setVisibility(visibility);
if (elevation > 0.0F && VERSION.SDK_INT >= 21) {
view.setElevation(elevation);
}
}
}
Then in your layout file of course you use your own component:
<yourpackage.LayoutGroup
android:id="#+id/group_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="view1,view2,view3"/>
Related
I'm having trouble with a BottomSheetDialogFragment I implemented some days back in a project I'm in.
What happens is that I have a BottomSheet which contains a SearchView and a Recyclerview. The dialog fragment shows correctly and stuff, all good there.
The problem starts when I use the SearchView to filter the Recyclerview's results since when there's 5 or less results, the keyboard is overlapping the now small Recyclerview.
I want to know if it's possible to keep the BottomSheet height as match_parent or something to fill the window or keep the Recyclerview big enough to avoid the keyboard "messing up" with the results. I use the following method to make the fragment expanded when it opens:
private fun expandBottomSheet() {
view?.viewTreeObserver?.addOnGlobalLayoutListener {
val dialog = dialog as BottomSheetDialog
val bottomSheet = dialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
val behavior = BottomSheetBehavior.from<View>(bottomSheet)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
And my XML for the sheet is this:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="#string/bottom_sheet_behavior">
<View
android:layout_width="52dp"
android:layout_height="7dp"
android:layout_gravity="center"
android:layout_marginTop="#dimen/size_small_4"
android:layout_marginBottom="#dimen/size_small_4"
android:background="#drawable/border_top_swipe_indicator" />
<androidx.appcompat.widget.SearchView
android:id="#+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:iconifiedByDefault="false"
app:queryHint="#string/text_type_your_query" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/border_top_white"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/list_item" />
</LinearLayout>
Thanks in advance for the help!
Edit:
The bottom sheet containing the Recyclerview and stuff is a child fragment (a fragment instantiated from another fragment.)
For anyone who is searching for this question, if you want to force your BottomSheetDialogFragment have full screen height, just wrap your bottom sheet content layout inside of this custom FrameLayout:
public class MatchParentFrameLayout extends FrameLayout {
public MatchParentFrameLayout(#NonNull Context context) {
super(context);
}
public MatchParentFrameLayout(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public MatchParentFrameLayout(#NonNull Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MatchParentFrameLayout(#NonNull Context context, #Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
In the manifest.xml file you have your activities declared there for your application. Inside of the <activity> block where this bottom sheet is hosted you can declare a window soft input mode so that the keyboard does not overlap the view - instead it pushes it up.
<activity
...
android:windowSoftInputMode="adjustResize|stateVisible"> ... </activity>
stateVisible: "The soft keyboard is visible when that's normally appropriate (when the user is navigating forward to the activity's main window)."
adjustResize: "The activity's main window is always resized to make room for the soft keyboard on screen."
Docs
That should work for you. It could be possible, depending on your views, for that to result in a poor UI and UX. If that is true, you could set a focus listener on the search view and, when it gains focus, programmatically set the state of the bottom sheet to expanded. See the answerhere.
Even though I'm not satisfied with the fix I came up to, I have to say it's working smoothly.
Basically I wrapped my bottomsheet with a ViewPager and it's not resizing.
I admit this is a hack and I'm hoping someone can provide a more decent answer to this. In the meantime, ViewPager with a single bottomsheet is the way to go.
You can set the height with the below code in On Activity created
view?.viewTreeObserver?.addOnGlobalLayoutListener {
val rect = Rect()
view?.getWindowVisibleDisplayFrame(rect)
val screenHeight = view?.rootView?.height
val keyPadHeight = screenHeight?.minus(rect.bottom)
if (screenHeight != null) {
if (keyPadHeight != 0) {
if (view?.paddingBottom != keyPadHeight) {
view?.setPadding(0, 0, 0, keyPadHeight!!)
}
} else {
if (view?.paddingBottom != 0) {
view?.setPadding(0, 0, 0, 0)
}
}
Hope this will work for you.
I am trying to replicate a behavior that the current Google Maps has which allows the bottom sheet to be revealed when sliding up from the bottom bar.
Notice in the recording below that I first tap on one of the buttons at the bottom bar and then slide up, which in turn reveals the sheet behind it.
I cannot find anywhere explained how something like this can be achieved. I tried exploring the BottomSheetBehavior and customizing it, but nowhere I can find a way to track the initial tap and then let the sheet take over the movement once the touch slop threshold is reached.
How can I achieve this behavior without resorting to libraries? Or are there any official Google/Android views that allow this behavior between two sections (the navigation bar and bottom sheet)?
Took some time but I found a solution based on examples and discussion provided by two authors, their contributions can be found here:
https://gist.github.com/davidliu/c246a717f00494a6ad237a592a3cea4f
https://github.com/gavingt/BottomSheetTest
The basic logic is to handle touch events in onInterceptTouchEvent in a custom BottomSheetBehavior and check in a CoordinatorLayout if the given view (from now on named proxy view) is of interest for the rest of the touch delegation in isPointInChildBounds.
This can be adapted to use more than one proxy view if needed, the only change necessary for this is to make a proxy view list and iterate the list instead of using a single proxy view reference.
Below follows the code example of this implementation. Do note that this is only configured to handle vertical movements, if horizontal movements are necessary then adapt the code to your need.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<com.example.tabsheet.CustomCoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/customCoordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.tabs.TabLayout
android:id="#+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#android:color/darker_gray">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 1" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 2" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 3" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 4" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 5" />
</com.google.android.material.tabs.TabLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#3F51B5"
android:clipToPadding="false"
app:behavior_peekHeight="0dp"
app:layout_behavior=".CustomBottomSheetBehavior" />
</com.example.tabsheet.CustomCoordinatorLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
final CustomCoordinatorLayout customCoordinatorLayout;
final CoordinatorLayout bottomSheet;
final TabLayout tabLayout;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
customCoordinatorLayout = findViewById(R.id.customCoordinatorLayout);
bottomSheet = findViewById(R.id.bottomSheet);
tabLayout = findViewById(R.id.tabLayout);
iniList(bottomSheet);
customCoordinatorLayout.setProxyView(tabLayout);
}
private void iniList(final ViewGroup parent) {
#ColorInt int backgroundColor;
final int padding;
final int maxItems;
final float density;
final NestedScrollView nestedScrollView;
final LinearLayout linearLayout;
final ColorDrawable dividerDrawable;
int i;
TextView textView;
ViewGroup.LayoutParams layoutParams;
density = Resources.getSystem().getDisplayMetrics().density;
padding = (int) (20 * density);
maxItems = 50;
backgroundColor = ContextCompat.getColor(this, android.R.color.holo_blue_bright);
dividerDrawable = new ColorDrawable(Color.WHITE);
layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
nestedScrollView = new NestedScrollView(this);
nestedScrollView.setLayoutParams(layoutParams);
nestedScrollView.setClipToPadding(false);
nestedScrollView.setBackgroundColor(backgroundColor);
linearLayout = new LinearLayout(this);
linearLayout.setLayoutParams(layoutParams);
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
linearLayout.setDividerDrawable(dividerDrawable);
for (i = 0; i < maxItems; i++) {
textView = new TextView(this);
textView.setText("Item " + (1 + i));
textView.setPadding(padding, padding, padding, padding);
linearLayout.addView(textView, layoutParams);
}
nestedScrollView.addView(linearLayout);
parent.addView(nestedScrollView);
}
}
CustomCoordinatorLayout.java
public class CustomCoordinatorLayout extends CoordinatorLayout {
private View proxyView;
public CustomCoordinatorLayout(#NonNull Context context) {
super(context);
}
public CustomCoordinatorLayout(
#NonNull Context context,
#Nullable AttributeSet attrs
) {
super(context, attrs);
}
public CustomCoordinatorLayout(
#NonNull Context context,
#Nullable AttributeSet attrs,
int defStyleAttr
) {
super(context, attrs, defStyleAttr);
}
#Override
public boolean isPointInChildBounds(
#NonNull View child,
int x,
int y
) {
if (super.isPointInChildBounds(child, x, y)) {
return true;
}
// we want to intercept touch events if they are
// within the proxy view bounds, for this reason
// we instruct the coordinator layout to check
// if this is true and let the touch delegation
// respond to that result
if (proxyView != null) {
return super.isPointInChildBounds(proxyView, x, y);
}
return false;
}
// for this example we are only interested in intercepting
// touch events for a single view, if more are needed use
// a List<View> viewList instead and iterate in
// isPointInChildBounds
public void setProxyView(View proxyView) {
this.proxyView = proxyView;
}
}
CustomBottomSheetBehavior.java
public class CustomBottomSheetBehavior<V extends View> extends BottomSheetBehavior<V> {
// we'll use the device's touch slop value to find out when a tap
// becomes a scroll by checking how far the finger moved to be
// considered a scroll. if the finger moves more than the touch
// slop then it's a scroll, otherwise it is just a tap and we
// ignore the touch events
private int touchSlop;
private float initialY;
private boolean ignoreUntilClose;
public CustomBottomSheetBehavior(
#NonNull Context context,
#Nullable AttributeSet attrs
) {
super(context, attrs);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
#Override
public boolean onInterceptTouchEvent(
#NonNull CoordinatorLayout parent,
#NonNull V child,
#NonNull MotionEvent event
) {
// touch events are ignored if the bottom sheet is already
// open and we save that state for further processing
if (getState() == STATE_EXPANDED) {
ignoreUntilClose = true;
return super.onInterceptTouchEvent(parent, child, event);
}
switch (event.getAction()) {
// this is the first event we want to begin observing
// so we set the initial value for further processing
// as a positive value to make things easier
case MotionEvent.ACTION_DOWN:
initialY = Math.abs(event.getRawY());
return super.onInterceptTouchEvent(parent, child, event);
// if the last bottom sheet state was not open then
// we check if the current finger movement has exceed
// the touch slop in which case we return true to tell
// the system we are consuming the touch event
// otherwise we let the default handling behavior
// since we don't care about the direction of the
// movement we ensure its difference is a positive
// integer to simplify the condition check
case MotionEvent.ACTION_MOVE:
return !ignoreUntilClose
&& Math.abs(initialY - Math.abs(event.getRawY())) > touchSlop
|| super.onInterceptTouchEvent(parent, child, event);
// once the tap or movement is completed we reset
// the initial values to restore normal behavior
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
initialY = 0;
ignoreUntilClose = false;
return super.onInterceptTouchEvent(parent, child, event);
}
return super.onInterceptTouchEvent(parent, child, event);
}
}
Result with transparent status bar and navigation bar to help visualize the bottom sheet sliding up, but excluded from the code above since it was not relevant for this question.
Note: It is possible you might not even need a custom bottom sheet behavior if your bottom sheet layout contains a certain scrollable view type (NestedScrollView for example) that can be used as is by the CoordinatorLayout, so try without the custom bottom sheet behavior once your layout is ready since it will make this simpler.
You could try something like this (It's Pseudocode, hopefully you understand what I'm getting at):
<FrameLayout id="+id/bottomSheet">
<View id="exploreNearby bottomMargin="buttonContainerHeight/>
<LinearLayout>
<Button id="explore"/>
<Button id="explore"/>
<Button id="explore"/>
</LinearLayout>
<View width="match" height="match" id="+id/touchCatcher"
</FrameLayout>
Add a gesture detector on the bottomSheet view on override onTouch(). which uses SimpleOnGestureListener to wait for a "scroll" events - everything but a scroll event you can replicate down through to the view as normal.
On a scroll event you can grow your exploreNearby as a delta (make sure it doesn't recurse or go to high or too low).
The Bottom sheet class will already do this for you. Just set it's peek height to 0 and it should already listen for the slide up gesture.
However, I'm not positive it will work with a peek height of 0. So if that doesn't work, simply put a peek height of 20dp and make the top portion of the bottom sheet layout transparent so it is not visible.
That should do the trick for ya, unless I'm misunderstanding your question. If your goal is to simply be able to tap at the bottom and slide upwards bringing up the bottom sheet that should be pretty straight forward.
The one possible issue that you "could" encounter is if the bottom sheet doesn't receive the touch events due to the button already consuming it. If this happens you will need to create a touch handler for the whole screen and return "true" that you are handling it each time, then simply forward the touch events to the underlying view, so when you get above the threshold of your bottom tab bar you start sending the touch events to the bottom sheet layout instead of the tab bar.
It sounds harder than it is. Most classes have an onTouch and you just forward it on. However, only go that route, if it doesn't work for you out of the box the way I described in the first two scenarios.
Lastly, one other option that might work is to create your tab buttons as part of the bottomSheetLayout and make the peek height equivalent of the tab bar. Then make sure the tab bar is constrained to bottomsheet parent bottom, so that when you swipe up it simply stays at the bottom. This would enable you to click the buttons or get the free bottom sheet behavior.
Happy Coding!
When using the translucent status and navigation bars from the new Android 4.4 KitKat APIs, setting fitsSystemWindows="true" and clipToPadding="false" to a ListView works initially. fitsSystemWindows="true" keeps the list under the action bar and above the navigation bar, clipToPadding="false" allows the list to scroll under the transparent navigation bar and makes the last item in the list scroll up just far enough to pass the navigation bar.
However, when you replace the content with another Fragment through a FragmentTransaction the effect of fitsSystemWindows goes away and the fragment goes under the action bar and navigation bar.
I have a codebase of demo source code here along with a downloadable APK as an example: https://github.com/afollestad/kitkat-transparency-demo. To see what I'm talking about, open the demo app from a device running KitKat, tap an item in the list (which will open another activity), and tap an item in the new activity that opens. The fragment that replaces the content goes under the action bar and clipToPadding doesn't work correctly (the navigation bar covers the last item in the list when you scroll all the way down).
Any ideas? Any clarification needed? I posted the before and after screenshots of my personal app being developed for my employer.
I struggled with the same problem yesterday. After thinking a lot, I found an elegant solution to this problem.
First, I saw the method requestFitSystemWindows() on ViewParent and I tried to call it in the fragment's onActivityCreated() (after the Fragment is attached to the view hierarchy) but sadly it had no effect. I would like to see a concrete example of how to use that method.
Then I found a neat workaround: I created a custom FitsSystemWindowsFrameLayout that I use as a fragment container in my layouts, as a drop-in replacement for a classic FrameLayout. What it does is memorizing the window insets when fitSystemWindows() is called by the system, then it propagates the call again to its child layout (the fragment layout) as soon as the fragment is added/attached.
Here's the full code:
public class FitsSystemWindowsFrameLayout extends FrameLayout {
private Rect windowInsets = new Rect();
private Rect tempInsets = new Rect();
public FitsSystemWindowsFrameLayout(Context context) {
super(context);
}
public FitsSystemWindowsFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FitsSystemWindowsFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected boolean fitSystemWindows(Rect insets) {
windowInsets.set(insets);
super.fitSystemWindows(insets);
return false;
}
#Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
super.addView(child, index, params);
tempInsets.set(windowInsets);
super.fitSystemWindows(tempInsets);
}
}
I think this is much simpler and more robust than hacks that try to determine the UI elements sizes by accessing hidden system properties which may vary over time and then manually apply padding to the elements.
I solved the issue by using the library I use the set the color of my translucent status bar.
The SystemBarConfig class of SystemBarTint (as seen here https://github.com/jgilfelt/SystemBarTint#systembarconfig) lets you get insets which I set as the padding to the list in every fragment, along with the use of clipToPadding="false" on the list.
I have details of what I've done on this post: http://mindofaandroiddev.wordpress.com/2013/12/28/making-the-status-bar-and-navigation-bar-transparent-with-a-listview-on-android-4-4-kitkat/
Okay, so this is incredibly weird. I just recently ran into this same issue except mine involves soft keyboard. It initially works but if I add fragment transaction, the android:fitsSystemWindows="true" no longer works. I tried all the solution here, none of them worked for me.
Here is my problem:
Instead of re-sizing my view, it pushes up my view and that is the problem.
However, I was lucky and accidentally stumbled into an answer that worked for me!
So here it is:
First of all, my app theme is: Theme.AppCompat.Light.NoActionBar (if that is relevant, maybe it is, android is weird).
Maurycy pointed something very interesting here, so I wanted to test what he said was true or not. What he said was true in my case as well...UNLESS you add this attribute to your activity in the android manifest of your app:
Once you add:
android:windowSoftInputMode="adjustResize"
to your activity, android:fitsSystemWindows="true" is no longer ignored after the fragment transaction!
However, I prefer you calling android:fitsSystemWindows="true" NOT on the root layout of your Fragment. One of the biggest places where this problem will occur is where if you have EditText or a ListView. If you are stuck in this predicament like I did, set android:fitsSystemWindows="true" in the child of the root layout like this:
YES, this solution works on all Lollipop and pre-lollipop devices.
And here is the proof:
It re-sizes instead of pushing the layout upwards.
So hopefully, I have helped someone who is on the same boat as me.
Thank you all very much!
A heads up for some people running into this problem.
A key piece of information with fitSystemWindows method which does a lot of the work:
This function's traversal down the hierarchy is depth-first. The same
content insets object is propagated down the hierarchy, so any changes
made to it will be seen by all following views (including potentially
ones above in the hierarchy since this is a depth-first traversal).
The first view that returns true will abort the entire traversal.
So if you have any other fragments with content views which have fitsSystemWindows set to true the flag will potentially be ignored. I would consider making your fragment container contain the fitsSystemWindows flag if possible. Otherwise manually add padding.
I've been struggling quite a bit with this as well.
I've seen all the responses here. Unfortunately none of them was fixing my problem 100% of the time.
The SystemBarConfig is not working always since it fails to detect the bar on some devices.
I gave a look at the source code and found where the insets are stored inside the window.
Rect insets = new Rect();
Window window = getActivity().getWindow();
try {
Class clazz = Class.forName("com.android.internal.policy.impl.PhoneWindow");
Field field = clazz.getDeclaredField("mDecor");
field.setAccessible(true);
Object decorView = field.get(window);
Field insetsField = decorView.getClass().getDeclaredField("mFrameOffsets");
insetsField.setAccessible(true);
insets = (Rect) insetsField.get(decorView);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
This is how to get them.
Apparently in Android L there'll be a nice method to get those insets but in the meantime this might be a good solution.
I encountered the same problem. When I replace Fragment.
The 'fitsSystemWindows' doesn't work.
I fixed by code add to your fragment
#Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
AndroidUtil.runOnUIThread(new Runnable() {
#Override
public void run() {
((ViewGroup) getView().getParent()).setFitsSystemWindows(true);
}
});
}
Combined with #BladeCoder answer i've created FittedFrameLayout class which does two things:
it doesn't add padding for itself
it scan through all views inside its container and add padding for them, but stops on the lowest layer (if fitssystemwindows flag is found it won't scan child deeper, but still on same depth or below).
public class FittedFrameLayout extends FrameLayout {
private Rect insets = new Rect();
public FittedFrameLayout(Context context) {
super(context);
}
public FittedFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FittedFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public FittedFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
protected void setChildPadding(View view, Rect insets){
if(!(view instanceof ViewGroup))
return;
ViewGroup parent = (ViewGroup) view;
if (parent instanceof FittedFrameLayout)
((FittedFrameLayout)parent).fitSystemWindows(insets);
else{
if( ViewCompat.getFitsSystemWindows(parent))
parent.setPadding(insets.left,insets.top,insets.right,insets.bottom);
else{
for (int i = 0, z = parent.getChildCount(); i < z; i++)
setChildPadding(parent.getChildAt(i), insets);
}
}
}
#Override
protected boolean fitSystemWindows(Rect insets) {
this.insets = insets;
for (int i = 0, z = getChildCount(); i < z; i++)
setChildPadding(getChildAt(i), insets);
return true;
}
#Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
super.addView(child, index, params);
setChildPadding(child, insets);
}
}
I have resolve this question in 4.4
if(test){
Log.d(TAG, "fit true ");
relativeLayout.setFitsSystemWindows(true);
relativeLayout.requestFitSystemWindows();
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}else {
Log.d(TAG, "fit false");
relativeLayout.setFitsSystemWindows(false);
relativeLayout.requestFitSystemWindows();
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
i havent found an answer on the internet for quite a while and now im asking you if you can help me.
Short:
How should i override addView() (or something else) to add Views defined in XML to my "custom view inflated XML Layout"
Long:
I want to create a custom view for my android app, so i created a clean subclass from RelativeLayout. In this, i let the Inflater load a xml layout to get a nice style.
But now, i want to add something inside the custom view, but dont want to add it programattically (this simply works), but with xml. I cant cross the gap in my mind to find the solution...
code:
Custom Class:
public class Slider extends RelativeLayout {
private RelativeLayout _innerLayout;
public Slider(Context context) {
super(context);
init();
}
public Slider(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
protected void init() {
LayoutInflater layoutInflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
_innerLayout = (RelativeLayout) layoutInflater.inflate(R.layout.layout_r, this);
}
#Override
public void addView(View child) {
//if (_innerLayout != null) _innerLayout.addView(child);
super.addView(child);
}
... all other addView's are overridden in the same way
XML File using the subclass:
<packagename....Slider
android:id="#+id/slider1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#color/Red" >
<TextView
android:id="#+id/heading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="HEADING" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="bbb" />
...
The TextView And Button are added to the subclass... sure... but after that, i got 3 children in Slider, the TextView, the Button and my inflated layout from R.layout.layout_r. But i just want 1 child (the layout_r) with Button and TextView in it.
As you can see in addView i tried to simply add the passed "View child" to the _innerLayout. That doesnt work. Android framework keeps calling addView and it ends with StackOverFlowError
Two thing to tell you too:
I know adding Views from XML doesnt call the given addView, but i've overriden all others too and all are looking the same so theres no need to show them.
Debugger said me, that addView is called BEFORE the _innerLayout gets the inflated Layout
Is 2. the reason?
Can u help me?
You can take look on how to inflate children into custom view here (vogella tutorial).
What you need is:
Define layout with children using <merge> tag
Inflate this layout in custom view constructor using LayoutInflater.inflate(res, this, true)
Just override your addView() method in your custom view Slider and check count of childs.
If getChildCount() == 0, then this is first addition and it is view initializing.
Kotlin example:
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
if (childCount == 0) {
super.addView(child, index, params)
} else {
// Do my own addition
}
}
I'm trying to create a custom Android control that contains a LinearLayout. You can think of it as an extended LinearLayout with fancy borders, a background, an image on the left...
I could do it all in XML (works great) but since I have dozens of occurences in my app it's getting hard to maintain. I thought it would be nicer to have something like this:
/* Main.xml */
<MyFancyLayout>
<TextView /> /* what goes inside my control's linear layout */
</MyfancyLayout>
How would you approach this? I'd like to avoid re-writing the whole linear layout onMeasure / onLayout methods. This is what I have for the moment:
/* MyFancyLayout.xml */
<TableLayout>
<ImageView />
<LinearLayout id="container" /> /* where I want the real content to go */
</TableLayout>
and
/* MyFancyLayout.java */
public class MyFancyLayout extends LinearLayout
{
public MyFancyLayout(Context context) {
super(context);
View.inflate(context, R.layout.my_fancy_layout, this);
}
}
How would you go about inserting the user-specified content (the TextView in main.xml) in the right place (id=container)?
Cheers!
Romain
----- edit -------
Still no luck on this, so I changed my design to use a simpler layout and decided to live with a bit of repeated XML. Still very interested in anyone knows how to do this though!
This exact question bugged me for some time already but it's only now that I've solved it.
From a first glance, the problem lies in the fact that a declarative content (TextView in Your case) is instantiated sometime after ctor (where we're usually inflating our layouts), so it's too early have both declarative and template content at hand to push the former inside the latter.
I've found one such place where we can manipulate the both: it's a onFinishInflate() method. Here's how it goes in my case:
#Override
protected void onFinishInflate() {
int index = getChildCount();
// Collect children declared in XML.
View[] children = new View[index];
while(--index >= 0) {
children[index] = getChildAt(index);
}
// Pressumably, wipe out existing content (still holding reference to it).
this.detachAllViewsFromParent();
// Inflate new "template".
final View template = LayoutInflater.from(getContext())
.inflate(R.layout.labeled_layout, this, true);
// Obtain reference to a new container within "template".
final ViewGroup vg = (ViewGroup)template.findViewById(R.id.layout);
index = children.length;
// Push declared children into new container.
while(--index >= 0) {
vg.addView(children[index]);
}
// They suggest to call it no matter what.
super.onFinishInflate();
}
A labeled_layout.xml referenced above is not unlike something like this:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation ="vertical"
android:layout_width ="fill_parent"
android:layout_height ="wrap_content"
android:layout_marginLeft ="8dip"
android:layout_marginTop ="3dip"
android:layout_marginBottom ="3dip"
android:layout_weight ="1"
android:duplicateParentState ="true">
<TextView android:id ="#+id/label"
android:layout_width ="fill_parent"
android:layout_height ="wrap_content"
android:singleLine ="true"
android:textAppearance ="?android:attr/textAppearanceMedium"
android:fadingEdge ="horizontal"
android:duplicateParentState="true" />
<LinearLayout
android:id ="#+id/layout"
android:layout_width ="fill_parent"
android:layout_height ="wrap_content"
android:layout_marginLeft ="8dip"
android:layout_marginTop ="3dip"
android:duplicateParentState="true" />
</LinearLayout>
Now (still omitting some details) elsewhere we might use it like this:
<com.example.widget.LabeledLayout
android:layout_width ="fill_parent"
android:layout_height ="wrap_content">
<!-- example content -->
</com.example.widget.LabeledLayout>
This approach saves me a lot of code! :)
As esteewhy explains, just swap the xml-defined contents into where you want them internally in your own layout, in onFinishInflate(). Example:
I take the contents that I specify in the xml:
<se.jog.custom.ui.Badge ... >
<ImageView ... />
<TextView ... />
</se.jog.custom.ui.Badge>
... and move them to my internal LinearLayout called contents where I want them to be:
public class Badge extends LinearLayout {
//...
private LinearLayout badge;
private LinearLayout contents;
// This way children can be added from xml.
#Override
protected void onFinishInflate() {
View[] children = detachChildren(); // gets and removes children from parent
//...
badge = (LinearLayout) layoutInflater.inflate(R.layout.badge, this);
contents = (LinearLayout) badge.findViewById(R.id.badge_contents);
for (int i = 0; i < children.length; i++)
addView(children[i]); //overridden, se below.
//...
super.onFinishInflate();
}
// This way children can be added from other code as well.
#Override
public void addView(View child) {
contents.addView(child);
}
Combined with custom XML attributes things gets very maintainable.
You can create your MyFancyLayout class by extending LinearLayout. Add the three constructors which call a method ("initialize" in this case) to set up the rest of the Views:
public MyFancyLayout(Context context) {
super(context);
initialize();
}
public MyFancyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public MyFancyLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
Within initialize, you do anything you need to to add the extra views. You can get the LayoutInflater and inflate another layout:
final LayoutInflater inflator = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflator.inflate(R.layout.somecommonlayout, this);
Or you can create Views in code and add them:
ImageView someImageView = new ImageView(getContext());
someImageView.setImageDrawable(myDrawable);
someImageView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
addView(someImageView);
If you're going to use the Context a lot, you can store a reference to it in your constructors and use that rather than getContext() to save a little overhead.
just use something like this:
<org.myprogram.MyFancyLayout>
...
</org.myprogram.MyFancyLayout>
Useful link - http://www.anddev.org/creating_custom_views_-_the_togglebutton-t310.html