Expand TouchDelegate beyond view's parent - android

I have a layout that looks like this:
and I am trying to expand clickable area for the green view. The code that I use for this purpose is:
public static void setTouchDelegate(View view, float dimen) {
final View parent = (View) view.getParent();
parent.post( () -> {
final Rect delegateArea = new Rect();
view.getHitRect(delegateArea);
delegateArea.right += dimen;
delegateArea.left -= dimen;
delegateArea.bottom += dimen;
delegateArea.top -= dimen;
parent.setTouchDelegate( new TouchDelegate( delegateArea , view));
});
}
The red area is the parent of the green view. The problem is that regardless how large TouchDelegate I am trying to set it always applies only within the parent, the red area. I can cover the whole red area to be clickable but I can't expand the clickable area into the blue area.
My assumption is that this is not a problem in my code but normal behaviour of TouchDelegate.
The question is: how can I set clickable area for the green view that looks like the purple area on the following preview ?
// edit:
This is layout of the item - the red rectangle with text and the green ImageButton:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/holo_red_dark"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:gravity="center_vertical"
android:paddingBottom="3dp"
android:paddingLeft="10dp"
android:paddingRight="4dp"
android:paddingTop="3dp">
<TextView
android:id="#+id/item_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingRight="18dp"
android:textColor="#android:color/black"
android:textSize="12sp"
android:textStyle="bold"
tools:text="Test" />
<ImageButton
android:id="#+id/item_green"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|center_vertical"
android:background="#android:color/transparent"
android:src="#android:color/holo_green_light" />
</FrameLayout>

Basically you need to set the delegateArea relative to the view' ancestor, and set the TouchDelegate on this ancestor.
In your image the target ancestor is the view with the blue background.
fun getRelativeLeft(view: View?, ancestor: View) : Int {
if (view == null)
return 0
return if (view.parent == ancestor)
view.left
else
view.left + getRelativeLeft(view.parent as? View, ancestor)
}
fun getRelativeTop(view: View?, ancestor: View) : Int {
if (view == null)
return 0
return if (view.parent == ancestor)
view.top
else
view.top + getRelativeTop(view.parent as? View, ancestor)
}
val relativeTop = Point(getRelativeLeft(view, ancestorView), getRelativeTop(view, ancestorView))
// View area in ancestor coordinates
val relativeRc = Rect(relativeTop.x, relativeTop.y, relativeTop.x + view.width, relativeTop.y + view.height)
// Increase touch area
relativeRc.inset(-offsetWidth, -offsetHeight)
// Set touch delegate on ancestorView with target view
ancestorView.touchDelegate = TouchDelegate(relativeRc, view)
Also it is better to use the modified TouchDelegate, due to a bug in the original version https://gist.github.com/patrickhammond/6d49e9ac08f96bd8d302#gistcomment-2385782

Related

How to make a popup view appear above a specific section an not above the whole activity

I made a keyboard with a key view. I want a preview to appear above each key of the keyboard without distorting the keyboard layout.
I made the Key view with a constraint layout and two card views:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:id="#+id/previewCard"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="#+id/letterCard"
app:layout_constraintEnd_toEndOf="#+id/letterCard"
app:layout_constraintStart_toStartOf="#+id/letterCard">
<TextView
android:id="#+id/previewTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:text="A" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="#+id/letterCard"
android:layout_width="match_parent"
android:layout_height="#dimen/KeyViewHeight"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:id="#+id/letterTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="1"
android:minWidth="20dp"
android:text="A" />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
In making the keyboard I created a Linear layout with each key in a row for flexibility and ease of adding another key (if you think there is a better way, I am all ears).
I saw something online about quick action dialog, but I have been unable to implement it. I have also been unable to implement a dialog the will only show on top of each key when it is pressed. THANK YOU!
Simply write this kotlin code, it will show popup above given view.
private fun showPopupWindow(anchor: View) {
PopupWindow(anchor.context).apply {
isOutsideTouchable = true
val inflater = LayoutInflater.from(anchor.context)
contentView = inflater.inflate(R.layout.popup_layout, null).apply {
measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
}
}.also { popupWindow ->
// Absolute location of the anchor view
val location = IntArray(2).apply {
anchor.getLocationOnScreen(this)
}
val size = Size(
popupWindow.contentView.measuredWidth,
popupWindow.contentView.measuredHeight
)
popupWindow.showAtLocation(
anchor,
Gravity.TOP or Gravity.START,
location[0] - (size.width - anchor.width) / 2,
location[1] - size.height
)
}
}
After reading through android's KeyboardView I found out that PopupWindow was used to show the preview for the keyboard. I implemented it like this:
PopupWindow popupWindow = new PopupWindow(context);
LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View popupView = layoutInflater.inflate(R.layout.preview_layout, null);
View parent = (View) keyView.getParent();
popupWindow.setContentView(popupView);
popupWindow.setBackgroundDrawable(null);
int popupPreviewY = parent.getTop();
int popupPreviewX = keyView.getLeft();
popupWindow.showAtLocation(keyView, Gravity.NO_GRAVITY, popupPreviewX, popupPreviewY);
The parent is to get the height where the key is. the getTop gets the top relative to the top of the parent

Get the position of each corner of a view, then add a view at the position

I am working on an android project where I have a custom view. When the custom view is clicked, I want a to put a view (a circle) at each corner of the view.
At the moment I'm just trying to get it work in the top left corner but it ends up in the middle.
Below is my click function for adding the view.
View view = LayoutInflater.from(getContext()).inflate(R.layout.view, this, false);
TextView textItem = view.findViewById(R.id.lblItemText);
textItem.setText("View: " + counter);
view.setOnClickListener(new OnClickListener()
{
#Override
public void onClick(View v)
{
Anchor anchor1 = new Anchor(getContext());
anchor1.setLeft(v.getLeft());
anchor1.setTop(CustomView.this.getTop());
CustomView.this.addView(anchor1);
}
});
The custom view is hosted inside a relative layout. The custom view extends RelativeLayout and the anchor view which is supposed to go into the top left corner of the custom view extends button.
The anchor constructor contains the following:
public Anchor(Context context)
{
super(context);
this.setBackgroundResource(R.drawable.anchor);
this.setPadding(0,0,0,0);
this.setWidth(1);
this.setHeight(1);
}
For some reason the anchor is appearing in the middle instead of being on the corner as shown below
Below is kind of expecting.
UPDATE
After a couple of days made some progress and I do have it working, except its using hardcoded values to get it in the right position, which doesn't seem right. I'm guessing this will only work on the specific device I'm testing on, another device with another resolution will be positioned wrong.
Below is the code I have that hopefully shows what is I am trying to achieve along with a screenshot as to what I have now.
private void createAnchorPoints()
{
//Main View
ViewGroup mainView = activity.findViewById(android.R.id.content);
int[] viewToBeResizedLoc = new int[2];
viewToBeResized.getLocationOnScreen(viewToBeResizedLoc);
//Add top left anchor
Anchor topLeftAnchor = new Anchor(context, Anchor.ResizeMode.TOP_LEFT);
FrameLayout.LayoutParams topLeftParms = new FrameLayout.LayoutParams(150,150);
topLeftParms.leftMargin = viewToBeResizedLoc[0] - 50;
topLeftParms.topMargin = viewToBeResizedLoc[1] - viewToBeResized.getHeight() - 30;
topLeftAnchor.setLayoutParams(topLeftParms);
mainView.addView(topLeftAnchor);
//Add top right anchor
Anchor topRightAnchor = new Anchor(context, Anchor.ResizeMode.TOP_RIGHT);
FrameLayout.LayoutParams topRightParms = new FrameLayout.LayoutParams(150, 150);
topRightParms.leftMargin = topLeftParms.leftMargin + viewToBeResized.getWidth() - 40;
topRightParms.topMargin = topLeftParms.topMargin;
topRightAnchor.setLayoutParams(topRightParms);
mainView.addView(topRightAnchor);
//Add bottom left anchor
Anchor bottomLeftAnchor = new Anchor(context, Anchor.ResizeMode.BOTTOM_LEFT);
FrameLayout.LayoutParams bottomLeftParms = new FrameLayout.LayoutParams(150, 150);
bottomLeftParms.leftMargin = topLeftParms.leftMargin;
bottomLeftParms.topMargin = topLeftParms.topMargin + viewToBeResized.getHeight() - 40;
bottomLeftAnchor.setLayoutParams(bottomLeftParms);
mainView.addView(bottomLeftAnchor);
//Add bottom right anchor
Anchor bottomRightAnchor = new Anchor(context, Anchor.ResizeMode.BOTTOM_RIGHT);
FrameLayout.LayoutParams bottomRightParms = new FrameLayout.LayoutParams(150, 150);
bottomRightParms.leftMargin = topRightParms.leftMargin;
bottomRightParms.topMargin = bottomLeftParms.topMargin;
bottomRightAnchor.setLayoutParams(bottomRightParms);
mainView.addView(bottomRightAnchor);
}
Since the top-level layout is a RelativeLayout, you will need to use the view positioning that is available to RelativeLayout to achieve what you want. (See the documentation.)
Here is a mock-up of what you want to achieve in XML. This mock-up will demonstrate how we can approach the actual solution. I am using standard views, but it shouldn't matter. The technique will apply to your custom views. The image is from Android Studio's designer, so no code was used to create the image.
activity_main.xml
<RelativeLayout
android:id="#+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<RelativeLayout
android:id="#+id/customView"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_centerInParent="true"
android:background="#android:color/holo_green_light" />
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignStart="#id/customView"
android:layout_alignTop="#id/customView"
android:src="#drawable/circle"
android:translationX="-10dp"
android:translationY="-10dp" />
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignEnd="#id/customView"
android:layout_alignTop="#id/customView"
android:src="#drawable/circle"
android:translationX="10dp"
android:translationY="-10dp" />
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignBottom="#id/customView"
android:layout_alignStart="#id/customView"
android:src="#drawable/circle"
android:translationX="-10dp"
android:translationY="10dp" />
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignBottom="#id/customView"
android:layout_alignEnd="#id/customView"
android:src="#drawable/circle"
android:translationX="10dp"
android:translationY="10dp" />
</RelativeLayout>
circle.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<!-- fill color -->
<solid android:color="#android:color/holo_red_light" />
<size
android:width="20dp"
android:height="20dp" />
</shape>
The Actual Solution
Now that we have demonstrated that the mocked-up approach works, we now have to reproduce the effect in code. We will have to add the circle view and position it within the parent RelativeLayout using RelativeLayout view positioning and translations. The following code shows just the top left circle positioned, but the other circles will be positioned in a similar way.
activity_main.java
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Drawable circle = ContextCompat.getDrawable(this, R.drawable.circle);
ImageView imageView = new ImageView(this);
imageView.setImageDrawable(circle);
int circleSize = dpToPx(CIRCLE_SIZE_DP);
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(circleSize, circleSize);
// Position top left circle within the custom view.
lp.addRule(RelativeLayout.ALIGN_START, R.id.customView);
lp.addRule(RelativeLayout.ALIGN_TOP, R.id.customView);
// Uncomment these 2 lines to position the top left circle with translation.
imageView.setTranslationX(-circleSize / 2);
imageView.setTranslationY(-circleSize / 2);
// Uncomment these 3 lines to position the top left circle with margins.
// View customView = findViewById(R.id.customView);
// lp.leftMargin = customView.getLeft() - circleSize / 2;
// lp.topMargin = customView.getTop() - circleSize / 2;
((RelativeLayout) findViewById(R.id.relativeLayout)).addView(imageView, lp);
}
private int dpToPx(int dp) {
return (int) (dp * getResources().getDisplayMetrics().density);
}
private static final int CIRCLE_SIZE_DP = 20;
}
The code above uses a shortened layout:
activity_main.xml
<RelativeLayout
android:id="#+id/relativeLayout"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<RelativeLayout
android:id="#+id/customView"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_centerInParent="true"
android:background="#android:color/holo_green_light" />
</RelativeLayout>
It is also possible to produce the same positioning using margins. The code to use margins is commented out but will work. (I think that negative margins may also work, but I have read that they are not officially supported, so I try to avoid them.)

Android RecyclerView ItemDecorator: align Inset Divider with List Item TextView

I would like to inset a RecyclerView ItemDecorator divider to left align with a TextView (the item title) inside the list element that is constrained to be 72dp from the parent left side. Something like we see here in the Google Material Design Docs. I assume that I need to somehow reference the layout params on the titleTextView, but am not sure how to do that from my ItemDecorator code, as I seem to be able to only get the params which is the ConstraintLayout, and not the textview itself. Any help or pointers in the right direction would be greatly appreciated! The relevant ItemDecorator code, where I try to get the textView param looks something like this:
for (int i = 0; i < recyclerView.getChildCount() - 1; ++i) {
View child = recyclerView.getChildAt(i);
RecyclerView.LayoutParams params =(RecyclerView.LayoutParams) child.getLayoutParams();
//this just gives me the constraint layout, not the TextView params, how to reference the textViewParams!?
The RecyclerView looks like this:
<android.support.v7.widget.RecyclerView
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/myList"
android:name="com.example.Something"
android:layout_width="0dp"
android:layout_height="0dp"
app:layoutManager="LinearLayoutManager"
tools:context=".controllers.fragments.ExampleFragment"
tools:listitem="#layout/fragment_something"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
The list item xml is a constraint layout with some TextViews, and the relevant portion looks like this:
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="72dp">
<ImageView
android:id="#+id/myImageView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginLeft="16dp"
app:layout_constraintLeft_toLeftOf="parent"
tools:src="#drawable/ic_menu_manage"
android:src="#drawable/ic_menu_manage"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="16dp" />
<TextView
android:id="#+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginLeft="72dp"
android:layout_marginTop="20dp"
android:text="MyTitle"
android:textAppearance="#style/TextAppearance.AppCompat.Subhead"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="#+id/textView5"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="SampleTitle" />
...
</android.support.constraint.ConstraintLayout>
use a custom item decorator.
class CustomItemDecoration(context: Context) : RecyclerView.ItemDecoration()
{
private val mDivider: Drawable = context.resources.getDrawable(R.drawable.custom_item_divider) // this is a shape that i want to use
override fun onDrawOver(c: Canvas,
parent: RecyclerView,
state: RecyclerView.State)
{
val left = parent.paddingLeft // change to how much padding u want to add here .
val right = parent.width - parent.paddingRight
val childCount = parent.childCount
for (i in 0 until childCount)
{
val child = parent.getChildAt(i)
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + mDivider.intrinsicHeight
mDivider.setBounds(left, top, right, bottom)
mDivider.draw(c)
}
}
}
you set it like that
recyclerView.addItemDecoration(CustomItemDecoration(context))
Since the ConstraintLayout is a ViewGroup, it is possible to access the child of the ConstraintLayout from inside of the ItemDecorator by simply doing something like:
public class MyItemDivider extends RecyclerView.ItemDecoration {
....some code here
#Override
public void onDrawOver(Canvas canvas, RecyclerView recyclerView,
RecyclerView.State state) {
super.onDrawOver(canvas, recyclerView, state);
for (int i = 0; i < recyclerView.getChildCount() - 1; ++i) {
ViewGroup item = (ViewGroup) recyclerView.getChildAt(i);
//for now the titleText is at index 1 of constraint layout
TextView titleChild = (TextView) item.getChildAt(1);
int titleTextLeftMargin = titleChild.getLeft();
//this would probably be less brittle
//but I would need to somehow change the context for this to be doable?
//View titleChild = item.findViewById(R.id.contractListProductNameTextView);
...some more code here
divider.setBounds(titleTextLeftMargin, top, right, bottom);
divider.draw(canvas);
}
}
}
Since you already know by how much you have to inset the divider from the left side (calculate it from your layout if you don't know), all you need to do is convert that dp value to px as shown here and set it as the left bound in your custom ItemDecoration.
public class InsetDividerDecoration extends RecyclerView.ItemDecoration {
private final int insetValueInDp = 72; // in your case
#Override
public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
int dividerLeft = (int) (insetValueInDp * Resources.getSystem().getDisplayMetrics().density);
...
}
}
And use it in your recycler view -
recyclerView.addItemDecoration(new InsetDividerDecoration());

ExpandableListView expandable list not expanding when its height is wrap_content, and something is below it

Here is my layout code. When i click on the expandable list view it does not expand. However, if I increase the height of list it expands. Is there a way to increase the height of list dynamically through code? Can i make it to expand in the middle of my layout. Because if I increase the height, it'll just add unnecessary white space in collapsed list.
Code:
<ScrollView 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">
<RelativeLayout 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:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context="com.pocketcash.pocketcash.HomeActivity">
<Switch
android:id="#+id/detailedCashInfoSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:onClick="cashToggleSwitchClicked"
android:text="Toggle Detailed Cash Info" />
<Button
android:id="#+id/addCashButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/expandableListView2"
android:layout_centerHorizontal="true"
android:onClick="launchAddCash"
android:text="Add cash" />
<Button
android:id="#+id/addExpenditureButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/addCashButton"
android:layout_centerHorizontal="true"
android:layout_marginTop="25dp"
android:onClick="launchAddExpenditure"
android:text="Add expenditure" />
<TextView
android:id="#+id/recentLogsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="#+id/addExpenditureButton"
android:layout_marginTop="50dp"
android:text="Recent Logs:"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="#+id/logText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="#+id/recentLogsText"
android:layout_marginTop="0dp"
android:text="Small Text"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="#+id/recentViewAllText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="#+id/recentLogsText"
android:layout_marginLeft="10dp"
android:layout_toEndOf="#+id/recentLogsText"
android:layout_toRightOf="#+id/recentLogsText"
android:onClick="recentViewLogClicked"
android:text="(view all)"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="#+id/addViewAllText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/addCashButton"
android:layout_centerHorizontal="true"
android:onClick="addViewLogClicked"
android:text="(view logs)"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="#+id/expenseViewAllText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/addExpenditureButton"
android:layout_centerHorizontal="true"
android:onClick="expenseViewLogClicked"
android:text="(view logs)"
android:textAppearance="?android:attr/textAppearanceSmall" />
<ImageView
android:id="#+id/userImage"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="#+id/detailedCashInfoSwitch"
android:layout_marginTop="34dp"
android:background="#drawable/default_user" />
<TextView
android:id="#+id/nameText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="#+id/userImage"
android:layout_marginLeft="25dp"
android:layout_toEndOf="#+id/userImage"
android:layout_toRightOf="#+id/userImage"
android:text="Name"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="#+id/logoutText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="#+id/nameText"
android:layout_alignStart="#+id/nameText"
android:layout_below="#+id/nameText"
android:clickable="true"
android:onClick="logUserOut"
android:text="logout"
android:textAppearance="?android:attr/textAppearanceSmall" />
<ImageButton
android:id="#+id/settingsButton"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_alignTop="#+id/nameText"
android:layout_marginLeft="25dp"
android:layout_toEndOf="#+id/nameText"
android:layout_toRightOf="#+id/nameText"
android:background="#drawable/settingsicon"
android:onClick="launchSettingsActivity" />
<fragment
android:id="#+id/fragment_currentBalanceInfo"
android:name="com.pocketcash.pocketcash.CurrentBalanceInfoFragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/userImage"
android:layout_centerHorizontal="true"
tools:layout="#layout/fragment_current_balance_info" />
<ExpandableListView
android:id="#+id/expandableListView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/fragment_currentBalanceInfo"
android:layout_centerHorizontal="true"
android:clickable="true" />
</RelativeLayout>
</ScrollView>
After much research on this topic, i could only reach the conclusion that a scrollable object like the expandable listview cant be declared within another scrollable view.
However i tweaked around with my java code a little bit for solving this issue during runtime.
expListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
#Override
public void onGroupExpand(int groupPosition) {
int height = 0;
for (int i = 0; i < expListView.getChildCount(); i++) {
height += expListView.getChildAt(i).getMeasuredHeight();
height += expListView.getDividerHeight();
}
expListView.getLayoutParams().height = (height+6)*10;
}
});
// Listview Group collapsed listener
expListView.setOnGroupCollapseListener(new ExpandableListView.OnGroupCollapseListener() {
#Override
public void onGroupCollapse(int groupPosition) {
expListView.getLayoutParams().height = 61;
}
});
This way i adjusted the height of the widget dynamically when it was expanded or collapsed. Its not a dependable method and it works on hit and trial so i ultimately had to move this widget to a completely new activity. However this did work flawlessly. But if anyone finds a better answer, please do share.
As Romain Guy (Google Engineer works on UI toolkit) Said in his post
By setting the width to wrap_content you are telling ListView to be as wide as the widest of its children. ListView must therefore measure its items and to get the items it has to call getView() on the Adapter. This may happen several times depending on the number of layout passes, the behavior of the parent layout, etc.
So if you set the layout width or layout height of your ListView to wrap_content the ListView will try to measure every single view that is attached to it - which is definitely not what you want.
Keep in mind: avoid setting wrap_content for ListViews or GridViews or ExpandableListView at all times, for more details see this Google I/O video talking about the world of listview
So Make ExpandableListView to match_parent
I know the question was already answered but I have another solution which could work for more cases: I set the initial height of the list view by counting the number of items, and then update this height on collapse/expand of group by retrieving children count of the expanded/collapsed group. Here is a sample code:
int item_size = 50; // 50px
int sub_item_size = 40; // 40px
expListView.getLayoutParams().height = item_size*myAdapter.getGroupCount();
expenseListView.setAdapter(myAdapter);
// ListView Group Expand Listener
expenseListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
#Override
public void onGroupExpand(int groupPosition) {
int nb_children = myAdapter.getChildrenCount(groupPosition);
expenseListView.getLayoutParams().height += sub_item_size*nb_children;
}
});
// Listview Group collapsed listener
expenseListView.setOnGroupCollapseListener(new ExpandableListView.OnGroupCollapseListener() {
#Override
public void onGroupCollapse(int groupPosition) {
int nb_children = myAdapter.getChildrenCount(groupPosition);
expenseListView.getLayoutParams().height -= sub_item_size*nb_children;
}
});
Normally we can't use ListView, GridView or ExpandableListView inside Scrollview. If you need it, then you have to give fixed height to your expandable ListView.
private void setListViewHeight(final ExpandableListView listView, final int groupPosition) {
final ExpandableListAdapter listAdapter = (ExpandableListAdapter) listView.getExpandableListAdapter();
int totalHeight = 0;
// be careful with unspecified width measure spec, it will ignore all layout params and even
// screen dimensions, so to get correct height, first get maximum width View can use and
// call measure() this way
final int desiredWidth = View.MeasureSpec.makeMeasureSpec(listView.getWidth(), View.MeasureSpec.EXACTLY);
for (int i = 0; i < listAdapter.getGroupCount(); i++) {
final View groupItem = listAdapter.getGroupView(i, false, null, listView);
groupItem.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED);
totalHeight += groupItem.getMeasuredHeight();
// count only expanded other groups or the clicked collapsed one
if (((listView.isGroupExpanded(i)) && (i != groupPosition))
|| ((!listView.isGroupExpanded(i)) && (i == groupPosition))) {
for (int j = 0, childrenNumber = listAdapter.getChildrenCount(i); j < childrenNumber; j++) {
final View listItem = listAdapter.getChildView(i, j, j == childrenNumber - 1, null, listView);
listItem.measure(desiredWidth, View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
totalHeight += listItem.getMeasuredHeight();
}
}
}
final ViewGroup.LayoutParams params = listView.getLayoutParams();
final int height = totalHeight
+ (listView.getDividerHeight() * (listAdapter.getGroupCount() - 1));
params.height = height;
listView.setLayoutParams(params);
listView.requestLayout();
}
and assign this method on list group click:
expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
#Override
public boolean onGroupClick(final ExpandableListView expandableListView, final View view, final int groupPosition, final long id) {
setListViewHeight(expandableListView, groupPosition);
return false;
}
});
IMPORTANT: although this code should work in most cases, I had some problems when child list items were TextViews with android:layout_height="wrap_content", hence their height depended on their width, which might be connected to this problem: ViewGroup{TextView,...}.getMeasuredHeight gives wrong value is smaller than real height. Please, write a comment if you manage to fix it.
Unfortunately, there doesn't seem to be a way to use wrap_content as the height of the ExpandableListView. Instead, setting a fixed height and android:nestedScrollingEnabled="true" worked for me.
I found a way better solution to achieve this phenomena with the following less complex and much efficient Kotlin code.
before all keep some points in mind :
Used fixed height Parent and Child views.
Extracted the heights of both Parent and Child into dimens.xml file.
Set divider='#null', scrollbars="none", groupIndicator="#null" (for customGroupIndicator, otherwise ignore) in ExpandableListView.
Never use paddingVertical including paddingTop and paddingBottom, replace them with layoutMargins for views having dynamic height.
Code Logic (For me this chunk is a part of a ViewModel Function named initUi(uiObject))
//Here `ui` is a DataBinding object.
//You can use ViewBinding or simple Android findViewById as per your comfort.
val data: List<ModelClass> = listObject
val context = ui.root.context
ui.expandableList.apply {
val mAdapter = MyExpandableListAdapter(data, context)
setAdapter(mAdapter)
//fetching parent height from dimens.xml
val parentViewHeight = ceil(
context.resources.getDimension(R.dimen.layout_menu_parent_height)
).toInt()
//fetching child height from dimens.xml
val childViewHeight = ceil(
context.resources.getDimension(R.dimen.layout_menu_child_height)
).toInt()
//Setting up the initial height of view when All parents are collapsed
layoutParams.height = data.size * parentViewHeight
setOnGroupExpandListener { groupPosition ->
//Increasing the height for Expanded SubCategory
layoutParams.height += mAdapter.getChildrenCount(groupPosition) * childViewHeight
}
setOnGroupCollapseListener { groupPosition ->
//Decreasing the height for Collapsed SubCategory
layoutParams.height -= mAdapter.getChildrenCount(groupPosition) * childViewHeight
}
}
MyExpandableListAdapter.kt
class FragmentMenuExpandableListAdapter(
val data: List<Data>, val context: Context
): BaseExpandableListAdapter() {
//Declaring inflater Once can save a lot of CPU and RAM uses...
private val inflater = LayoutInflater.from(context)
override fun getGroupCount(): Int = data.size
override fun getChildrenCount(groupPosition: Int): Int = data[groupPosition].subcategories.size
override fun getGroup(groupPosition: Int): Data = data[groupPosition]
override fun getChild(groupPosition: Int, childPosition: Int) = data[groupPosition].subcategories[childPosition]
override fun getGroupId(groupPosition: Int): Long = groupPosition.toLong()
override fun getChildId(groupPosition: Int, childPosition: Int): Long = "$groupPosition$childPosition0".toLong()
override fun hasStableIds(): Boolean = true
override fun getGroupView(
groupPosition: Int,
isExpanded: Boolean,
convertView: View?,
parent: ViewGroup
): View {
val binding = LayoutMenuParentBinding.inflate(inflater, parent, false)
binding.apply {
val temp = getGroup(groupPosition)
title.text = temp.category_name
//here I used a custom ImageView as GroupIndicator
//So following code chunk is Optional
if(temp.subcategories.isEmpty()) dropdownIcon.visibility = View.GONE
else dropdownIcon.let { icon ->
val view = parent as ExpandableListView
icon.setImageResource(
if(view.isGroupExpanded(groupPosition)) {
icon.setOnClickListener { view.collapseGroup(groupPosition) }
R.drawable.ic_state_expanded
}
else {
icon.setOnClickListener {
view.expandGroup(groupPosition)
}
R.drawable.ic_state_collapsed
}
)
}
root.setOnClickListener {
TODO("Do on Parent click")
}
}
return binding.root
}
override fun getChildView(
groupPosition: Int,
childPosition: Int,
isLastChild: Boolean,
convertView: View?,
parent: ViewGroup?
): View {
val binding = LayoutMenuChildBinding.inflate(
inflater, parent, false
).apply {
val temp = getChild(groupPosition, childPosition)
title.text = temp.categoryName
root.setOnClickListener {
TODO("Do on Child click")
}
}
return binding.root
}
override fun isChildSelectable(groupPosition: Int, childPosition: Int): Boolean = true
}

Asymmetric RelativeLayout behavior in Android

The following two layout files produce different results:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<View
android:id="#+id/box"
android:background="#ff0000"
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="#+id/next_box" />
<View
android:id="#+id/next_box"
android:background="#0000ff"
android:layout_width="60dp"
android:layout_alignParentRight="true"
android:layout_height="30dp"
/>
</RelativeLayout>
</LinearLayout>
Result:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<View
android:id="#+id/box"
android:background="#ff0000"
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_alignParentLeft="true"
/>
<View
android:id="#+id/next_box"
android:background="#0000ff"
android:layout_width="60dp"
android:layout_alignParentRight="true"
android:layout_height="30dp"
android:layout_toRightOf="#+id/box"
/>
</RelativeLayout>
</LinearLayout>
Result:
Both layouts are trying to describe the same constraints. Namely, the red rectangle should touch the left edge of the parent, the blue rectangle should touch the right edge of the parent, and they should appear next to each other horizontally. The only difference is whether you specify the "next to" constraint on the red rectangle or the blue rectangle. I figured out the reason which has to do with the measure resolution order generated by forming a dependency graph of the constraints, but I only figured it out through reading RelativeLayout's source code, and I couldn't find any documentation / notes regarding this behavior. Since RelativeLayout must be a commonly used layout component, is there a more intuitive explanation for this behavior, or is there some part of documentation that I am missing?
Although both seem to describe the same constraints, they actually don't. The difference is that one says, red must sit next to blue, while the other says blue must sit next to red. One means that where ever red goes blue must follow, the other says, where ever blue goes red must follow, and they both want to go to different places.
In the first instance, red box depends on the blue box, so the blue box gets constructed first. The blue box has a width of 60dp, so a 60dp blue box is constructed first and aligned right. Then comes the red box, which has a constraint to sit next to the blue box. Width 0 is ignore because it needs to sit next to 60dp blue and align left.
In the second instance, blue box depends on the red box, so the red box gets constructed first. The red box says it wants 0dp and align left, so it can't be seen. Then comes the blue box which needs to sit next to invisible red and align right, thus occupying the entire space, its width ignored.
Hope this makes sense :)
All this parameters defined in:
android.widget.RelativeLayout
private void applyHorizontalSizeRules(LayoutParams childParams, int myWidth, int[] rules) {
RelativeLayout.LayoutParams anchorParams;
// VALUE_NOT_SET indicates a "soft requirement" in that direction. For example:
// left=10, right=VALUE_NOT_SET means the view must start at 10, but can go as far as it
// wants to the right
// left=VALUE_NOT_SET, right=10 means the view must end at 10, but can go as far as it
// wants to the left
// left=10, right=20 means the left and right ends are both fixed
childParams.mLeft = VALUE_NOT_SET;
childParams.mRight = VALUE_NOT_SET;
anchorParams = getRelatedViewParams(rules, LEFT_OF);
if (anchorParams != null) {
childParams.mRight = anchorParams.mLeft - (anchorParams.leftMargin +
childParams.rightMargin);
} else if (childParams.alignWithParent && rules[LEFT_OF] != 0) {
if (myWidth >= 0) {
childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
}
}
anchorParams = getRelatedViewParams(rules, RIGHT_OF);
if (anchorParams != null) {
childParams.mLeft = anchorParams.mRight + (anchorParams.rightMargin +
childParams.leftMargin);
} else if (childParams.alignWithParent && rules[RIGHT_OF] != 0) {
childParams.mLeft = mPaddingLeft + childParams.leftMargin;
}
anchorParams = getRelatedViewParams(rules, ALIGN_LEFT);
if (anchorParams != null) {
childParams.mLeft = anchorParams.mLeft + childParams.leftMargin;
} else if (childParams.alignWithParent && rules[ALIGN_LEFT] != 0) {
childParams.mLeft = mPaddingLeft + childParams.leftMargin;
}
anchorParams = getRelatedViewParams(rules, ALIGN_RIGHT);
if (anchorParams != null) {
childParams.mRight = anchorParams.mRight - childParams.rightMargin;
} else if (childParams.alignWithParent && rules[ALIGN_RIGHT] != 0) {
if (myWidth >= 0) {
childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
}
}
if (0 != rules[ALIGN_PARENT_LEFT]) {
childParams.mLeft = mPaddingLeft + childParams.leftMargin;
}
if (0 != rules[ALIGN_PARENT_RIGHT]) {
if (myWidth >= 0) {
childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
}
}
}
The view left and right edges ( childParams.mLeft, childParams.mRight) calculations based on anchor view parameters (anchorParams). From this code childParams.mRight edge of the view defined by LEFT_OF (android:layout_toLeftOf) can be recalculated by ALIGN_RIGHT (android:layout_alignRight) or ALIGN_PARENT_RIGHT (android:layout_alignParentRight). Here is explanation why 0-width red view become more than 0.
<View
android:id="#+id/box"
android:background="#ff0000"
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="#+id/next_box"/>
Right edge of this view defined by LEFT_OF:
childParams.mRight = anchorParams.mLeft - (anchorParams.leftMargin +
childParams.rightMargin);
In this case anchor view is:
<View
android:id="#+id/next_box"
android:background="#0000ff"
android:layout_width="60dp"
android:layout_alignParentRight="true"
android:layout_height="30dp"
/>
the left edge of this view 60dp from the right side of the screen margings not defined => childParams.mRight = screen_width - 60dp
Left edge of this view defined by ALIGN_PARENT_LEFT:
childParams.mLeft = mPaddingLeft + childParams.leftMargin;
the left edge of this view left edge of anchor view is 0 because android:layout_alignParentLeft="true" and margins not defined => childParams.mLeft = 0
the same calculation can be done for the second example:
childParams.mRight = screen_width
childParams.mLeft = 0

Categories

Resources