I have an expandablelistview which contains multiple viewpagers, and each viewpager contains multiple pages which each have a linearlayout of data. The data in the linearlayout is dynamic (there could be 5 rows or there could be 20), and each row has a dynamic height (it could have 1 line of text or 2-3).
As the viewpager requires a hard-coded height to be set for it to display, I need to display all of the content in each page's linearlayout, and I can't allow the content within the viewpager to scroll, I'm in the difficult situation of trying to calculate the height of the viewpager's active page view and then dynamically update the height of the viewpager so that the whole linearlayout content is visible. As a new page is swiped to, it should then update the viewpager height accordingly.
I'm attempting to do this within the pager adapter's SetPrimaryItem method, however the height that is coming back is close but not accurate. In my pager page view I have an outer relativelayout with a blue background, and then an inner one with a red background. For some reason the outer (blue) layout is going maybe 70dp further than the bottom of the data, I'm guessing the pager is manipulating its height behind-the-scenes. The inner one (red) behaves correctly and wraps the data within it, so this is the one I'm trying to calculate the height of so I can update the viewpager's height, however its height is incorrectly being calculated as bigger than it is, so after I update the viewpager height there is empty space at the bottom. See the screenshot, as it probably makes more sense than what I just typed:
Another acceptable solution would be to calculate the longest page's height and set the viewpager to that once, and then don't update it as the page changes, but I haven't gone that route yet and would still run into the same issue calculating the correct height.
I've spent a day on this already, any ideas / suggestions for how I can accurately update the viewpager height to fit its contents would be appreciated. I've tried a lot of other solutions on SO and haven't found one that actually worked. Here is where I've gotten so far with it:
Pager view:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v4.view.ViewPager
android:id="#+id/property_details_pager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
Pager adapter: (SetPrimaryItem method is where I try to update the pager's height dynamically)
public class PropertyDetailsPagerAdapter : PagerAdapter {
private ViewPager pager;
private List<List<KeyValuePair<string, string>>> lists;
public PropertyDetailsPagerAdapter (View view, List<List<KeyValuePair<string, string>>> lists) {
this.pager = view.FindViewById<ViewPager> (Resource.Id.property_details_pager);
this.pager.OffscreenPageLimit = lists.Count;
this.lists = lists;
}
public void Update (List<List<KeyValuePair<string, string>>> lists) {
this.pager.Adapter = null;
this.lists = lists;
this.pager.Adapter = this;
this.pager.Adapter.NotifyDataSetChanged ();
}
#region Setup Views
public override Java.Lang.Object InstantiateItem (View collection, int position) {
var layoutInflater = (LayoutInflater)ApplicationContext.Activity.GetSystemService (Context.LayoutInflaterService);
var view = layoutInflater.Inflate (Resource.Layout.PropertyDetailsPagerItem, null);
var pagerList = view.FindViewById<LinearLayout> (Resource.Id.property_details_pager_list);
var list = this.lists [position];
new PropertyDetailsPagerListAdapter (pagerList, Resource.Layout.PropertyDetailsPagerKeyValueRow, list);
var listViewSwipe = view.FindViewById<LinearLayout> (Resource.Id.property_details_pager_swipe);
listViewSwipe.Visibility = this.lists.Count > 1 ? ViewStates.Visible : ViewStates.Gone;
this.pager.AddView(view);
return view;
}
public override void SetPrimaryItem (View container, int position, Java.Lang.Object #object) {
base.SetPrimaryItem (container, position, #object);
var view = this.pager.GetChildAt (position);
var innerView = view.FindViewById<RelativeLayout> (Resource.Id.property_details_pager_item);
innerView.Measure (0, 0);
var parameters = this.pager.LayoutParameters as RelativeLayout.LayoutParams;
parameters.Height = innerView.MeasuredHeight;// TODO: Why isn't this height accurate?!?!
this.pager.LayoutParameters = parameters;
}
#endregion
#region Infrastructure
// Other irrelevant methods
public override int Count {
get {
return this.lists.Count;
}
}
#endregion
public List<List<KeyValuePair<string, string>>> Lists { get { return this.lists; } }
}
View for each page within the pager:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/blue">
<RelativeLayout
android:id="#+id/property_details_pager_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/red">
<LinearLayout
android:id="#+id/property_details_pager_swipe"
android:layout_below="#+id/dotted"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:visibility="invisible" />
<RelativeLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:background="#color/light_gray">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Swipe for\nmore records"
android:textColor="#color/gray"
android:gravity="right"
android:padding="#dimen/padding" />
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:id="#+id/property_details_pager_list"
android:layout_below="#+id/property_details_pager_swipe"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</RelativeLayout>
</RelativeLayout>
Adapter for each page within the pager:
public class PropertyDetailsPagerListAdapter
{
private int resourceId;
private List<KeyValuePair<string, string>> keyValuePairs;
public PropertyDetailsPagerListAdapter (LinearLayout pagerList, int resourceId, List<KeyValuePair<string, string>> keyValuePairs) {
this.resourceId = resourceId;
this.keyValuePairs = keyValuePairs;
pagerList.RemoveAllViews ();
foreach (var keyValuePair in this.keyValuePairs) {
pagerList.AddView (GetView (keyValuePair));
}
}
public View GetView (KeyValuePair<string, string> keyValuePair) {
var layoutInflater = (LayoutInflater)ApplicationContext.Activity.GetSystemService (Context.LayoutInflaterService);
var view = layoutInflater.Inflate(resourceId, null);
// Assumes Resource.Layout.PropertyDetailsPagerKeyValueRow
view.FindViewById<TextView> (Android.Resource.Id.Text1).Text = keyValuePair.Key;
view.FindViewById<TextView> (Android.Resource.Id.Text2).Text = keyValuePair.Value;
return view;
}
}
I was able to figure it out. The issue was that I was using innerView.Measure (0,0) and then getting the MeasuredHeight, however that was giving a bogus height. After calling Measure correctly, which entails passing a real Width into it, the correct Height is returned!!
public override void SetPrimaryItem (View container, int position, Java.Lang.Object #object) {
base.SetPrimaryItem (container, position, #object);
var view = this.pager.GetChildAt (position);
var innerView = view.FindViewById<RelativeLayout> (Resource.Id.property_details_pager_item);
var desiredWidth = MeasureSpec.MakeMeasureSpec (this.pager.Width, MeasureSpecMode.AtMost);
innerView.LayoutParameters = new RelativeLayout.LayoutParams (RelativeLayout.LayoutParams.WrapContent, RelativeLayout.LayoutParams.WrapContent);
innerView.Measure (desiredWidth, (int)MeasureSpecMode.Unspecified);
var parameters = this.pager.LayoutParameters as RelativeLayout.LayoutParams;
parameters.Height = innerView.MeasuredHeight;
this.pager.LayoutParameters = parameters;
}
Related
Here is what I am trying to do:
What is the simplest way to create rows that scroll together and are composed of variable sized clickable Views with the same height on Android
Basically create variable width columns that have the same width in every row. Also need to add, delete and add listeners. Seems like a fairly simple task, but I am finding Android's GUI library a lot harder to figure out than Java's and WPF's GUI library.
Here is my RecyclerView:
public class MainActivity extends AppCompatActivity {
private RecyclerView ampRecyclerView;
private RecyclerView.Adapter ampAdapter;
private RecyclerView.LayoutManager ampLayoutManager;
List<FunctionView> myDataset = new ArrayList<FunctionView>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setUpRecyclerView();
}
private void setUpRecyclerView() {
LinearLayout linearLayout = findViewById(R.id.main_ll);
linearLayout.setWillNotDraw(false);
ampRecyclerView = (RecyclerView) findViewById(R.id.AmpRecyclerView);
// use a linear layout manager
ampLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
ampRecyclerView.setLayoutManager(ampLayoutManager);
myDataset.add(new FunctionView(this));
myDataset.add(new FunctionView(this));
myDataset.add(new FunctionView(this));
// specify an adapter
ampAdapter = new MainActivityAdapter(myDataset, 1);
ampRecyclerView.setAdapter(ampAdapter);
}
}
My adapter
class MainActivityAdapter extends RecyclerView.Adapter<MainActivityAdapter.FunctionViewHolder> {
private List<FunctionView> views = new ArrayList<FunctionView>();
private List<LinearLayout> llViews = new ArrayList<>();
private int rows;
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder
public static class FunctionViewHolder extends RecyclerView.ViewHolder {
// each data item is just a string in this case
public LinearLayout linearLayout;
public FunctionViewHolder(LinearLayout v) {
super(v);
linearLayout = v;
}
}
// Provide a suitable constructor (depends on the kind of dataset)
public MainActivityAdapter(List<FunctionView> myDataset, int rows) {
views = myDataset;
this.rows = rows;
}
// Create new views (invoked by the layout manager)
#Override
public MainActivityAdapter.FunctionViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
LinearLayout linearLayout = (LinearLayout) LayoutInflater.from(parent.getContext())
.inflate(R.layout.function_holder, parent, false);
llViews.add(linearLayout);
MainActivityAdapter.FunctionViewHolder vh = new MainActivityAdapter.FunctionViewHolder(linearLayout);
return vh;
}
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(MainActivityAdapter.FunctionViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
int width = 0;
for(FunctionView fv : views){
holder.linearLayout.addView(fv);
width += fv.getWidth();
}
holder.linearLayout.setMinimumWidth(width);
//TODO set the data
// holder.functionView = views.get(position);
}
// Return the size of your dataset (invoked by the layout manager)
#Override
public int getItemCount() {
return rows;
}
I know there are glaring design flaws. I am trying to get the scrolling working first, because every layout I try doesn't work how I'd like.
Here is the layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/main_ll"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/AmpRecyclerView"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
and the holder:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:isScrollContainer="true"
android:nestedScrollingEnabled="true"
>
</LinearLayout>
as you said there are glaring design flaws, but you need to change the android:layout_height to wrap_content and android:layout_width to match_Parent.
if your item's hight is match_parent then your inner layout's hight becomes the recyclerview's hight then there is no room for other items so there will be no scrolling.
also, put something like a textView in it to be able to see the items.
another note, the name is supposed to be item not holder. holder is related to ViewHolder which is a totally different thing. you can name it according to your activity for example if Your activity name is MainActivity so your activity layout is activity_main, then you can call the inner layouts item_main
I recommend watching a tutorial on youtube or read an article from medium or anywhere (you can simply just google android recyclerview example) to learn the basics.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:id="#+id/txt_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="SAMPLE"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:textSize="30sp"
/>
</LinearLayout>
I'm dealing with legacy code that uses a viewpager to display an horizontal list of images(instead of say a listview or recyclerview) in a fragment. The viewpager uses a custom PagerAdapter which overrides the appropriate methods(instantiateItem, getItemPositiion etc). My issue right now is that I want to add some space at the end of the final element of the viewpager, I've attempted to do this by adding a margin to this final elment.
public Object instantiateItem(ViewGroup collection, int position) {
FrameLayout itemView = ViewManager.createView(position, height, type);
int viewWidth = itemView.getLayoutParams().width;
RelativeLayout itemViewWrap = new RelativeLayout(mContext);
if(postion == items.size()-1){
RelativeLayout.LayoutParams itemViewparams = RelaiveLayout.LayoutParams)itemView.getLayoutParams();
itemViewparams.setMargins(0, 0, viewWidth, 0);
}
itemViewWrap.addView(itemView);
collection.addView(itemViewWrap);
return itemView;
}
Unfortunately this does not work and the final element is just not shown. How do I properly achieve this? Here is the xml for the fragment in which this viewpager resides.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.playground.test.views.ItemViewPager
android:id="#+id/viewpager"
android:layout_gravity="center_vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
</RelativeLayout>
You could override the last page's width using ViewPagerAdapter# getPageWidth. To be more than the rest of the page's with the margin you want.
https://developer.android.com/reference/android/support/v4/view/PagerAdapter.html#getPageWidth(int)
I inflate another layout to appear below some view in my current layout.
This is done like this:
LayoutInflater vi = (LayoutInflater) getActivity().getApplicationContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View rootView = vi.inflate(R.layout.horizontal_scroll_view, null);
horizontalScrollView = (HorizontalScrollView) rootView.findViewById(R.id.hsv_suggestions_scroll_view);
LinearLayout suggestionsContainer = (LinearLayout) horizontalScrollView.findViewById(R.id.ll_suggestions_container);
and I can confirm that it appears in the right place since I add some Views in it after a while and they all appear.
The layout I inflate is :
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/hsv_suggestions_scroll_view"
android:scrollbars="none" android:layout_gravity="center_horizontal"
android:paddingTop="16dp" android:fillViewport="true"
android:layout_width="match_parent" android:layout_height="wrap_content">
<LinearLayout
android:id="#+id/ll_suggestions_container"
android:gravity="center_horizontal" android:orientation="horizontal"
android:layout_width="wrap_content" android:layout_height="wrap_content">
</LinearLayout>
just a HorizontalScrollView with a LinearLayout as a child.
The Views I add later are all of them TextViews.
Now after a user action (write some text on an editText) I'm trying to scroll to that View and highlight it. Highlight works. What does not work is scroll.
I have tried :
horizontalScrollView.postDelayed(new Runnable() {
#Override
public void run() {
horizontalScrollView.smoothScrollTo(scrollTo, 0);
}
}, 300);
where variable scrollTo is what I get when I apply getLeft() to the View I wanna scroll to. I can confirm that it takes various values.
Anyone can help me with that ?
Got a similar issue.
I guess the root cause is that the UI was not yet rendered at that moment, causing the scroll not to work properly.
I just had to wrap my call to smoothScrollTo into .post as follows:
mScrollView.post(new Runnable() {
#Override
public void run() {
mScrollView.smoothScrollTo(xxx, yyy);
}
});
Notice that I do a post directly on the scrollView to make sure it is executed after it is being rendered. For example, doing a getActivity().runOnUIThread() would not work in my case.
Switch to using a RecyclerView with a LinearLayoutManager with orientation set to Horizontal. Like this:
scroll_view.xml
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/scrollView"
android:scrollbars="none"
android:layout_gravity="center_horizontal"
android:paddingTop="16dp"
android:fillViewport="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
android:orientation="horizontal">
<LinearLayout
android:id="#+id/scrollContainer"
android:gravity="center_horizontal"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</android.support.v7.widget.RecyclerView>
Note: the layoutManager tag is required here for the layout to inflate. After it's inflated, we're going to set it programatically as well because otherwise we'll get an exception because the RecyclerView basically disposes of it before it's done with it.
OptionAdapter.java
public class OptionAdapter extends RecyclerView.Adapter<OptionAdapter.OptionHolder> {
private Context context;
private LayoutInflater inflater;
private ArrayList<String> options;
public OptionAdapter(Context context) {
this.context = context;
this.inflater = LayoutInflater.from(context);
options = new ArrayList<>();
}
#Override
public OptionHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new OptionHolder(inflater.inflate(R.layout.textview, parent, false));
}
#Override
public void onBindViewHolder(OptionHolder holder, int position) {
String option = options.get(position);
((TextView) holder.itemView).setText(option);
}
#Override
public int getItemCount() {
return options != null ? options.size() : 0;
}
public ArrayList<String> getOptions() {
return options;
}
public void addOption(String option, Integer index) {
if (index != null && index <= options.size()) {
options.add(index, option);
} else {
options.add(option);
}
notifyDataSetChanged();
}
public class OptionHolder extends RecyclerView.ViewHolder {
public OptionHolder(View itemView) {
super(itemView);
}
}
}
text_view.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
Setting everything up
final LayoutInflater inflater = LayoutInflater.from(this);
final RecyclerView scrollView = (RecyclerView) inflater.inflate(R.layout.scroll_view, container, false);
scrollView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
final OptionAdapter adapter = new OptionAdapter(this);
scrollView.setAdapter(adapter);
container here is whatever view is going to be holding the RecyclerView. In my code, I've got it in a LinearLayout.
Adding a view to the list
adapter.addOption("The Added One", null);
Or if you want to add it to a specific position in the list.
adapter.addOption("The Added One", position);
Scrolling to a specific position
scrollView.smoothScrollToPosition(position);
Scrolling to a specific item in the list
scrollView.smoothScrollToPosition(adapter.getOptions().indexOf("ItemText"));
Hope it works for you!
Instead of smoothScrollTo(), try using scrollTo().
Make sure that your getLeft() is really returning a value > 0;
I am trying to create a nested comments system, but I have encountered an issue. I have a main ListView to display all the top level comments, and every comment XML contains a sub ListView with adjusted padding. The nested comments load, but only the first returned comment loads for each parent comment. I have done some debugging and it appears as if all the data is being sent to the ListView ArrayAdapter, but getView() is only being run for the first item. Is it possible to force a reload of the ListView, or is there a better way to do what I am trying?
Below is my current code:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="-20dp"
android:layout_marginRight="-20dp"
android:background="#ff2b2b2b"
android:orientation="vertical"
android:padding="20dp"
android:paddingBottom="0dp"
android:paddingEnd="0dp"
android:scrollbars="none">
<TextView
android:id="#+id/author"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ccrama"
android:textSize="12dp" />
<TextView
android:id="#+id/commentLine"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="COMMENT"
android:textSize="14dp" />
<ListView
android:id="#+id/commentsListUnder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="-20dp"
android:layout_marginLeft="-5dp"
android:layout_marginRight="-20dp"
android:background="#ff2b2b2b"
android:orientation="vertical"
android:scrollbars="none" />
My ArrayAdapter looks like this
public class CommentAdapter extends ArrayAdapter<CommentNode> {
public CommentAdapter(Context context, ArrayList<CommentNode> users) {
super(context, 0, users);
main = context;
this.users = users;
}
ArrayList<CommentNode> users;
Context main;
CommentNode user;
#Override
public View getView(int position, View convertView, ViewGroup parent) {
user = getItem(position);
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.comment, parent, false);
}
TextView author = (TextView) convertView.findViewById(R.id.author);
TextView comm = (TextView) convertView.findViewById(R.id.commentLine);
TextView upvote = (TextView) convertView.findViewById(R.id.upvotePost);
author.setText(user.getComment().getAuthor());
comm.setText(user.getComment().getBody());
upvote.setText(user.getComment().getScore() + "");
ArrayList<CommentNode> comments = new ArrayList<CommentNode>();
CommentNode comment = user;
for (CommentNode node : comment.walkTree(TraversalMethod.BREADTH_FIRST)) {
if (node.getParent().getComment().getId() == comment.getComment().getId()) {
comments.add(node);
}
}
//The above method IS returning all the correctly leveled comments
CommentAdapter adapter = new CommentAdapter(main, comments);
ListView listView = (ListView) convertView.findViewById(R.id.commentsListUnder);
listView.setAdapter(adapter);
return convertView;
}
#Override
public int getCount() {
return users == null ? 0 : users.size();
} }
Thank you for helping!
This can be done without using ExpandableListView.
What is happening is as you have emebedded a list view inside another, it is getting shrunk, what you need to do is dynamically adjust the height of your inner list view based on the number of items. Do this in your outside adapter:
OrderDetailAdapter adapter = new OrderDetailAdapter(context,0,orderDetails);
int numberOfItems = orderDetails.size();
ViewGroup.LayoutParams params = holder.listView.getLayoutParams();
int pixels = (int) (80 * scale);
int dpPixels = (int) (0.1 * scale);
params.height =numberOfItems * pixels + (numberOfItems - 1) * dpPixels;
holder.listView.setLayoutParams(params);
holder.listView.setAdapter(adapter);
Where OrderDetailAdapter is adapter for the inner list.
The scale is obtained internally:
scale = context.getResources().getDisplayMetrics().density;
which is a float variable;
I solved this problem by using ExpandableListView instead of ListView.
This a ListView screenshot of my problem:
This is the layout XML:
<LinearLayout
android:id="#+id/viewer_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/background_dark"
android:orientation="vertical" >
<EditText
android:id="#+id/viewer_filter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableRight="#android:drawable/ic_menu_search"
android:hint="#string/hint_filter"
android:background="#android:color/white"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="3dp"
android:inputType="text"
android:paddingLeft="4dp"
android:selectAllOnFocus="true" >
</EditText>
<EditText
android:id="#+id/viewer_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableRight="#android:drawable/ic_menu_search"
android:hint="#string/hint_search"
android:background="#android:color/white"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_marginTop="3dp"
android:layout_marginBottom="5dp"
android:inputType="text"
android:paddingLeft="4dp"
android:selectAllOnFocus="true" >
</EditText>
</LinearLayout>
<HorizontalScrollView
android:id="#+id/viewer_hscroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/viewer_top" >
<ListView
android:id="#+id/viewer_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</ListView>
</HorizontalScrollView>
There are 3 problems in this scenario:
The Horizontal scrollview does not cover the full screen width (I drew a thick red line to mark the end)
The Horizontal scrollview does not scroll horizontally
The ListView rows are not of uniform width (this can be seen by the background color ending) (see the getView code below for details)
private static final int listRowLayout = android.R.layout.activity_list_item;
private Map<String, Integer> mColors = new HashMap<String, Integer>();
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// No logs here to keep ListView performance good
ViewHolder holder;
int color;
if( convertView == null ) {
convertView = mInflater.inflate(listRowLayout, parent, false);
holder = new ViewHolder();
holder.text = (TextView) convertView.findViewById(android.R.id.text1);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
String data = mData.get(position);
// A compiled regex is faster than String.Contains()
Matcher m = ViewHolder.regex.matcher(data);
if( m.find() ) {
color = mColors.get(m.group(1));
} else {
color = mColors.get("V");
}
holder.text.setText(data);
holder.text.setBackgroundColor(color);
return convertView;
}
private static class ViewHolder {
TextView text;
static Pattern regex = Pattern.compile(" ([VIDWEF])/");
}
}
I encountered the exact same issue in trying to display a log file. I have a dedicated activity to display the log file:
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_log);
// Read in lines from the log file
File clientLogFile = new File(LOG_FILE);
ArrayList<String> lines = new ArrayList<String>();
try
{
Scanner scanner = new Scanner((Readable) new BufferedReader(new FileReader(clientLogFile)));
try
{
while(scanner.hasNextLine())
{
lines.add(scanner.nextLine());
}
}
finally
{
scanner.close();
}
}
catch (FileNotFoundException e)
{
lines.add("No log file");
}
// Create a simple adaptor that wraps the lines for the ListView
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.list_item,lines);
// Create a ListView dynamically to overide onMeasure()
final ListView listView = new ListView(this)
{
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
// Override onMeasure so we can set the width of the view to the widest line in the log file
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Find maximum width of item in list and set scroll width equal to that
int maxWidth = 0;
for(int i=0; i<getAdapter().getCount(); i++)
{
View listItem = getAdapter().getView(i, null, this);
listItem.measure(0, 0);
int width = listItem.getMeasuredWidth();
if(width > maxWidth)
{
maxWidth = width;
}
}
// Set width of measured dimension
setMeasuredDimension(maxWidth, getMeasuredHeight());
}
};
// Add to scroll view
HorizontalScrollView horizontalScrollView = (HorizontalScrollView)findViewById(R.id.logScrollView);
horizontalScrollView.addView(listView);
// Set adaptor
listView.setAdapter(adapter);
// Enable fast scroll
listView.setFastScrollEnabled(true);
// Scroll to end
listView.post(new Runnable(){
public void run() {
listView.setSelection(listView.getCount() - 1);
}});
}
The onCreate method reads the log file and then dynamically adds a ListView to a HorizontalScrollView with onMeasure() overridden. The onMeasure() code determines the maximum width of the views in the adaptor and sets the ListView width to be that.
My activity_view_log.xml layout file is therefore very simple:
<?xml version="1.0" encoding="utf-8"?>
<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="5dp"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:paddingTop="5dp"
>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/logScrollView">
</HorizontalScrollView>
</RelativeLayout>
In order to have finer grained control of the lines in the ListView I give my adapter my own layout file in list_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#android:id/text1"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:inputType="text|none"
/>
At the end of by onCreate() I enable fast scroll and also scroll to the end of the lines in the log file.
I would probably reverse what you are doing. Create a ListView and make each item in the listview horizontally scrollable. This way items only scroll when they need to, and it does not scroll the entire screen. And you get complete control over the dimensions of each list item. To do this use a custom listview adapter as mentioned in the comments. There is also a possible duplicate of your question here: Android horizontal scroll list
In order to solve the 3 problems I had to make all the components (the horizontal scroll view, the list view and it's items) have a "fill_parent" width (I think it's the same as "match_parent"). In addition I had the listview's onMeasure(...) overridden to calculate the max width of it's items and set it via setMeasuredDimension(...). This will measure the view by it's widest item, not by it's first, as it is implemented now.
This is the solution I found.
The root of all evil :-) is that ListView is not designed to efficiently deal with rows of different length. To determine the ListView width, instead of looking at all the rows, only 3 rows are taken as average.
So, if the 3 rows are by chance short rows, the width will be clipped for the longer rows, it explains the problems I experienced.
To bypass this I calculated the maximum row length for all data, and I padded shorter rows with spaces, it solved all 3 problems I described in the question.
The code for padding (executed inside getView() )
holder.text.setText(String.format("%1$-" + mLen + "s", data));