Commonsware Drag Drop shrinks row height permanently - android

I did get the drag and drop working and the TouchListView class works great. However in my case I have rows of various height due to my adapter which contains an EditText that can have multiple lines. Therefore after I drop, all my rows convert to the tlv:normal_height which in my case is 74dip. This causes many rows to cut off all my text in the EditTexts. I tried re initializing my adapter (mylistview.setAdapter= myadapter), setting the ListView to GONE then VISIBLE and invalidateViews() but nothing seems to reset the ListView back to before I dragged, short of leaving the activity and coming back. What can be done here? -Thx
tlv:normal_height="74dip"
tlv:expanded_height="128dip"

There's little question that the original AOSP code was designed for uniform row heights, and the whole expanded_height construct was there to provide space for the user to visualize where the drop would occur.
One starting point would probably be to create a TouchListAdapter mixin interface (akin to SpinnerAdapter) where the normal_height and expanded_height would be retrieved dynamically from the adapter based on position as opposed to being fixed values declared in the layout. Whether that alone would be sufficient or more work would need to be done, I can't say.
If you come up with a solution, patches are welcome. Otherwise, I'll probably take a look at this sometime, but not very soon.
My apologies for not having a near-term silver bullet.

I edited the unExpandViews() method - called getAdapter() and for every item in my adapter set the height to 0 and then all the rows were set back to original. I also bypassed the delete part of the method since it did not apply to me.
private void unExpandViews(boolean deletion) {
int height_saved = 0;
CheckBoxifiedTextListAdapter cbla = (CheckBoxifiedTextListAdapter)getAdapter();
for (int i = 0;i < cbla.getCount(); i++)
{
//View v = getChildAt(i);
View v = cbla.getView(i, null, null);
//if (v == null)
//{
/*
if (deletion)
{
// HACK force update of mItemCount
int position = getFirstVisiblePosition();
int y = getChildAt(0).getTop();
setAdapter(getAdapter());
setSelectionFromTop(position, y);
// end hack
}
layoutChildren(); // force children to be recreated where needed
v = getChildAt(i);
if (v == null)
{
break;
}
height_saved = v.getHeight();
*/
//}
//else
//height_saved = v.getHeight();
if (isDraggableRow(v))
{
ViewGroup.LayoutParams params = v.getLayoutParams();
params.height = 0;
v.setLayoutParams(params);
v.setVisibility(View.VISIBLE);
}
}
}

Related

Android View.requestRectangleOnScreen: Scroll-error in API-23! How, do I work around it?

I am developing my first Android App and after a good start, I have spent days of deep debugging on a problem, which by now seems to be an error in the implementation of View.requestRectangleOnScreen in API-23 and probably many levels before that. Just now, I have discovered that the implementation of this routine is changed significantly in API-25.
The problem is that a request for focus on an EditText placed inside a HorizontalScrollView may cause the HorizontalScrollView to scroll away from the field requesting the focus.
In my case it is an EditText with centered text, which is then placed in the center of 1048576 pixels and scrolled roughly half a million pixels to the right making the text centered and visible (this part is perfectly ok!) But then this offset of half a million pixels is propagated up the parent chain and makes the HorizontalScrollView move to its far right and far away from the input field.
I have tracked it down to the View.requestRectangleOnScreen routine, which in the API-23 sources is as follows:
public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) {
if (mParent == null) {
return false;
}
View child = this;
RectF position = (mAttachInfo != null) ? mAttachInfo.mTmpTransformRect : new RectF();
position.set(rectangle);
ViewParent parent = mParent;
boolean scrolled = false;
while (parent != null) {
rectangle.set((int) position.left, (int) position.top,
(int) position.right, (int) position.bottom);
scrolled |= parent.requestChildRectangleOnScreen(child,
rectangle, immediate);
if (!child.hasIdentityMatrix()) {
child.getMatrix().mapRect(position);
}
position.offset(child.mLeft, child.mTop);
if (!(parent instanceof View)) {
break;
}
View parentView = (View) parent;
position.offset(-parentView.getScrollX(), -parentView.getScrollY());
child = parentView;
parent = child.getParent();
}
return scrolled;
}
The idea is to make the rectangle visible by scrolling it onto the screen in every containing View, starting at the leaf level and passing the request up the chain of parents. The initial rectangle is given in child coordinates, which of course have to be adjusted as we work our way up the chain of parents. This is done with the statement
position.offset(-parentView.getScrollX(), -parentView.getScrollY());
close to the end of the code above.
What I have found, is that this is wrong because we are transforming the position given in child coordinates using the scroll X/Y values pertaining to the parent coordinates. Using the scroll X/Y of the child instead solved my problem but it was not possible to make a perfect override of this routine because it relies on private member variables. Specifically, I found no way of mimicing the mAttachInfo.
Now, digging a bit further, I found that the code for this routine in API-25 has changed significantly and (IMHO) correctly to the following:
public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) {
if (mParent == null) {
return false;
}
View child = this;
RectF position = (mAttachInfo != null) ? mAttachInfo.mTmpTransformRect : new RectF();
position.set(rectangle);
ViewParent parent = mParent;
boolean scrolled = false;
while (parent != null) {
rectangle.set((int) position.left, (int) position.top,
(int) position.right, (int) position.bottom);
scrolled |= parent.requestChildRectangleOnScreen(child, rectangle, immediate);
if (!(parent instanceof View)) {
break;
}
// move it from child's content coordinate space to parent's content coordinate space
position.offset(child.mLeft - child.getScrollX(), child.mTop -child.getScrollY());
child = (View) parent;
parent = child.getParent();
}
return scrolled;
}
The most important change being the line
position.offset(child.mLeft - child.getScrollX(), child.mTop -child.getScrollY());
where the scroll X/Y adjustment is now made with child values.
Now, I have two questions.
First, do you agree with my observations above?
Second, how do I implement an App that can be used on both API-23 and API-25 under the given circumstances?
My current thoughts are to sub class the EditText and override the requestRectangleOnScreen method such that when the API is 25 and above, it just calls the super class method and when the API is below 25, I basically do a full override using code along the lines of the code from API-25 but then missing out on the mAttachInfo part.

How to check programatically if all the Views fit inside the screen

I have a layout which is something like this:
LinearLayout (linearLayout)
'--TextView (textView1)
'--ImageView (imageView)
'--TextView (textView2)
textView1 changes its text sometimes and it can be long, so it leaves part of textView2 out of the screen. I want to prevent that, so I want to remove imageView from the layout whenever this happens. imageView may or may not be visible at the time when this is computed (maybe it was removed before when textView1 was edited previously).
This is what I have coded:
void changeText(String veryLongString){
textView1.setText(veryLongString);
int [] loc = new int [2];
textView2.getLocationOnScreen(loc);
int bottom = textView2.getMeasuredHeight() + loc[1];
if (imageView.getVisibility() == View.GONE)
bottom += imageView.getHeight();
if (bottom > linearLayout.getMeasuredHeight()){
imageView.setVisibility(View.GONE);
} else {
imageView.setVisibility(View.VISIBLE);
}
}
But for some reason this doesn't work as expected, because it seems as if changes in the position and height of the Views don't take place immediately. When I call getMeasuredHeight() and getLocationOnScreen() I get the values BEFORE the changes I have just made. The result that I get is that if I set a very large text imageView is not removed, but if I then set a short text, it is removed.
If there any other way to face this problem?
Even though I think that this is not the right approach (you can do all kinds of stuff in your XML so you don't have to meddle with Java code), here is a quick example of what you can do from Java (for example, in your onStart() method)
ViewGroup group = (ViewGroup) findViewById(R.id.myLayout);
int groupHeight = group.getHeight();
for (int i = 0; i < group.getChildCount(); i++) {
groupHeight -= group.getChildAt(i).getHeight();
if (groupHeight < 0) {
// they don't fit in the layout
myImageView.setVisibility(View.GONE);
}
}

When is the correct moment to call getWidth() / getHeight() on a View?

EDIT --> Please, this is NOT a question WHY getWidth() / getHeight() return zero.
I have a Fragment inside an Activity.
I am dynamically adding SubViews (red) to a LinearLayout (RowView, blue) in horizontal orientation that is a child of the Fragment (green). How many of those childviews (SubViews) are added to the LinearLayout is determined at runtime.
The SubViews must have a fixed width of 200dp. Therefore, when dynamically adding the SubViews to the Linearlayout, I want to check if there is enough space for another SubView, or if a new Row needs to be started. The width of the RowView should be variable, and is NOT necessarily equal to the screen size.
In order to check if there is enough space, I simply see if the combined width of all SubViews is smaller than the width of the Linearlayout - the width of one SubView. Inside my custom LinearLayout (RowView), this looks as follows:
public void addSubView(SubView v) {
int childcount = getChildCount();
int totalChildWidth = 0;
for(int i = 0; i < childcount; i++) {
totalChildWidth += getChildAt(i).getWidth();
}
if(totalChildWidth < getWidth() - 200) { // there is space left in this row
addView(v);
}
}
The problem simply is, that getWidth() of the LinearLayout, and getWidth() of already added SubViews return 0, and therefore, the check if there is enough space goes wrong.
The reason for that is that I am calling getWi´dtdh() when the Views (the UI) have not yet been rendered on the screen, so my question is when is the right time to call my addSubView() method? Currently, I am calling it when creating the Fragment, which is obviously wrong:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment, container, false);
RowView row = (RowView) v.findViewById(R.id.rowView);
ArrayList<SubView> subViews = DBManager.getSubViewList(); // connects to a database and returns all available subviews
for(int i = 0; i < subViews.size(); i++) {
row.addSubView(subViews.get(i));
}
return v;
}
So where to call my addSubView(...) method where it is ensured that getWidth() inside it will not return 0? And in general, when is the correct moment (which callback method, according to Activity lifecycle) for getWidth() or getHeight() of a View to be called, where it is ensured that they will not return 0?
What I have tried so far:
Call addSubView(...) of my RowView in Fragments onActivityCreated(...) --> doesn't work
Connect to the database inside the RowViwes onSizeChanged(...) method, and all addSubView(...) there --> doesn't work
Do it as described in the code above, but with a Handler with 500ms delay --> works, because UI is rendered, but is not a proper solution for me
What you try to do is not working as the layouting isn't finished at the moment you want to know the width of the element. There are two solutions. The first one is to determine the actual width of your layout, which is ease in your case, as it is the actual screen width:
int width = getActivity().getResources().getDisplayMetrics().widthPixels
Use this value to determine how many of your subviews you can add in one row by converting the 200dp into pixel:
int viewWidth = (int) (200 * (getActivity().getResources().getDisplayMetrics().densityDpi / 160f));
Then you can calculate the maximal number of views for a row:
int maxViewsToAdd = (int) width/viewWidth;
The other solution is a globalLayoutListener, you can find a description here. But this seems not to work in all cases.

Creating and removing views in java

I am creating lots of view dynamically and adding them to my RelativeLayout. I need to keep track of all of these views so I can remove them later, so I add them is an ArrayList. But when I try to remove them all from the layout, they are not all removed.
ArrayList<LineView> lineChain = new ArrayList<LineView>();
LineView linkLine;
RelativeLayout wrapper; // Removed params etc.
// Later on in code
// This occurs many times
linkLine = new LineView(getApplicationContext());
wrapper.addView(linkLine, rlp);
lineChain.add(linkLine);
This is what I do when I try to remove all of the views. This only happens once:
for (int i = 0; i <= lineChain.size() -1; i++) {
LineView lv = lineChain.get(i);
wrapper.removeView(lv);
lineChain.remove(i);
}
As I said, the problem is that not all the lines are removed - I havn't managed to work out the pattern for which are deleted and which aren't.
You have a bug in your remove code.
for (int i = 0; i <= lineChain.size() -1; i++) {
LineView lv = lineChain.get(i);
wrapper.removeView(lv);
lineChain.remove(i);
}
The documentation for ArrayList.remove(int) function says:
Removes the element at the specified position in this list. Shifts any
subsequent elements to the left (subtracts one from their indices).
When you remove an item at an index i, all the remaining elements are shifted to the left. So for example if you remove an element at position 0. The element at position 1 is shifted to position 0 and is never removed (because you are incrementing i).
The your code to this:
while (!lineChain.isEmpty()) {
LineView lv = lineChain.get(0);
wrapper.removeView(lv);
lineChain.remove(0);
}
I think that the problem is while removing the element from the List after the cycle.
You should do something like this:
for (LineView lv : lineChain) {
((RelativeLayout) namebar.getParent()).removeView(namebar);
}
If you want to get rid of all the elements, just reset the list
lineChain = new ArrayList<LineView>()

A ListView where a specific row can always be scrolled to the top?

I want to be able to take a ListView and have a specific row be scrollable to the top of that Listview's bounds, even if the row is near the end and normally wouldn't be able to scroll that high in a normal android ListView (similar to how twitter works when you drill into a specific tweet and that tweet is always scrollable to the top even when there's nothing underneath it.)
Is there any way I can accomplish this task easily? I've tried measuring the row i want to scroll to the top and applying bottom padding to account for the extra space it would need, but that yields odd results (i presume because changing padding and such during the measure pass of a view is ill advised). Doing so before the measure pass doesn't work since the measured height of the cell in question (and any cells after it) hasn't happened yet.
Looks like you the setSelectionFromTop method of listview.
mListView.setSelectionFromTop(listItemIndex, 0);
I figured it out; its a bit complex but it seems to work mostly:
public int usedHeightForAndAfterDesiredRow() {
int totalHeight = 0;
for (int index = 0; index < rowHeights.size(); index++) {
int height = rowHeights.get(rowHeights.keyAt(index));
totalHeight += height;
}
return totalHeight;
}
#Override
public View getView(int position, View convertView, final ViewGroup parent) {
View view = super.getView(position, convertView, parent);
if (measuringLayout.getLayoutParams() == null) {
measuringLayout.setLayoutParams(new AbsListView.LayoutParams(parent.getWidth(), parent.getHeight()));
}
// measure the row ahead of time so that we know how much space will need to be added at the end
if (position >= mainRowPosition && position < getCount()-1 && rowHeights.indexOfKey(position) < 0) {
measuringLayout.addView(view, new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
measuringLayout.measure(MeasureSpec.makeMeasureSpec(parent.getWidth(), MeasureSpec.EXACTLY), MeasureSpec.UNSPECIFIED);
rowHeights.put(position, view.getMeasuredHeight());
measuringLayout.removeAllViews();
view.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
}
if (position == getCount()-1 && view.getLayoutParams().height == 0) {
// we know how much height the prior rows take, so calculate the last row with that.
int height = usedHeightForAndAfterDesiredRow();
height = Math.max(0, parent.getHeight() - height);
view.getLayoutParams().height = height;
}
return view;
}
This is in my adapter. It's a subclass of a merge adapter, but you can just put it in your code and substitute the super call with however you generate your rows.
the first if statement in getView() sets the layout params of a frame layout member var that is only intended for measuring, it has no parent view.
the second if statement calculates all the row heights for rows including and after the position of the row that I care about scrolling to the top. rowHeights is a SparseIntArray.
the last if statement assumes that there is one extra view with layout params already set at the bottom of the list of views whose sole intention is to be transparent and expand at will. the usedHeightForAndAfterDesiredRow call adds up all the precalculated heights which is subtracted from the parent view's height (with a min of 0 so we don't get negative heights). this ends up creating a view on the bottom that expands at will based on the heights of the other items, so a specific row can always scroll to the top of the list regardless of where it is in the list.

Categories

Resources