1

I am about to lose the last bits of patience I have with this ListView. It seems that whomever built it didn't think of my scenario:

  1. The list have about 5 visible rows on the activity layout
  2. The list should be scrolled with buttons: "Down" - for selecting the next item. "Up" for choosing the previous.
  3. The selected item must be visible. If possible centered in the view. Meaning that when pressing up/down buttons, the list would scroll and the item would stay in place.

What I already have:

  1. A list with adapter, and a selector drawable that's working using the
    listview.getChildAt( index ).setSelected( true );
  2. A not very reliable method of scrolling the list: using:
    smoothScrollToPositionFromTop( mCurSel, 0, 100 );

It is not reliable because once it gets to an item with none standard height (not as the other items in the list) it starts messing up the selection/scrolling and gets to the point the list starts to behave pretty wierd.

Now, is there any alternative (ready, opensource) that can do what I want? I am just about to re-write that list again WITHOUT ListView.

Paul Lammertsma
  • 37,593
  • 16
  • 136
  • 187
rubmz
  • 1,947
  • 5
  • 27
  • 49
  • 1
    ScrollView. scrollTo (0, 'Position of your list view item'); http://developer.android.com/reference/android/widget/ScrollView.html#scrollTo%28int,%20int%29 – Sai Phani Apr 08 '15 at 18:19
  • The principle behind ListViews is that they are scrolled through gestures and there is no need for up or down buttons. If you're designing for a non-touch screen, ListView copes with it already. – Paul Lammertsma Apr 08 '15 at 18:45
  • Furthermore, ListViews can also cope with choice modes (see the [choiceMode](http://developer.android.com/reference/android/widget/AbsListView.html#attr_android:choiceMode) attribute) and can show selection states (for instance, using [state list drawables](http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList)). – Paul Lammertsma Apr 08 '15 at 18:49
  • As I wrote the buttons are a requirement.. and yes, it's non standard. And probably the reason why it's buggy... – rubmz Apr 08 '15 at 19:06

2 Answers2

2

Maybe it's possible for ListView, but not very easily.. Anyway, if working with a RecyclerView from the v7 support package, there's a nice post here how to do what I want:

tracking-selected-item-in-recyclerview

SO answer leading there:

how-to-properly-highlight-selected-item-on-recyclerview

And just to save time to anyone trying to do this simple yet so complex tast of getting a sane list with selection working properly... so basic, here's my explanation:

  1. Obtain the "support v7" package by using the SDK manager.

  2. If working in Android Studio, adding the v7 jar to the project should be a breeze, with eclipse it's like walking on fire and drinking acid at the same time (no fun at all) - to make things short: copy the following jars from the sdk repository:

a. sdk/.../extra/...v4/android-support-v4.jar to your project under "libs", b. Do the same for sdk/.../extra/...v7/..RecyclerView/recyclerview-v7-22.0.0.jar c. And sdk/.../extra/...v7/..CardView/cardview-v7-22.0.0.jar d. Merge sdk/.../extra/...v7/..CardView/values.xml into your project's values.xml

Up till here - just to add the RecyclerView to your project ... hhhh .. so much work! ;) And I am saving you the hours of research...

Note: The layout editor won't recognise the RecyclerView, so you will have to add it manually (and it will continue to shout that this is a bad class... bla bla bla)

  1. Main Activity layout:

 <?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="@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.example.listtester.MainActivity" >
 
     <android.support.v7.widget.RecyclerView
         android:id="@+id/my_recycler_view"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_above="@+id/up_button"
         android:scrollbars="vertical" />
 
     <Button
         android:id="@+id/up_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignLeft="@+id/my_recycler_view"
         android:layout_alignParentBottom="true"
         android:text="Up" />
 
     <Button
         android:id="@+id/down_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentBottom="true"
         android:layout_toRightOf="@+id/up_button"
         android:text="Down" />
 
 </RelativeLayout>
  1. item layout:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="?android:attr/listPreferredItemHeight"
    android:padding="6dip" 
    android:background="@drawable/list_item_selector">

    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:layout_alignParentBottom="true"
        android:layout_alignParentTop="true"
        android:layout_marginRight="6dip"
        android:contentDescription="TODO"
        android:src="@drawable/ic_launcher" />

    <TextView
        android:id="@+id/secondLine"
        android:layout_width="fill_parent"
        android:layout_height="26dip"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_toRightOf="@id/icon"
        android:ellipsize="marquee"
        android:singleLine="true"
        android:text="Description"
        android:textSize="12sp" />

    <TextView
        android:id="@+id/firstLine"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_above="@id/secondLine"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_alignWithParentIfMissing="true"
        android:layout_toRightOf="@id/icon"
        android:gravity="center_vertical"
        android:text="Example application"
        android:textSize="16sp" />

</RelativeLayout> 
  1. add colors.xml to res/values folder

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="pressed_color">#ffffff00</color>
    <color name="default_color">#ffffffff</color>
    
</resources>
  1. add list_item_drawable.xml

<?xml version="1.0" encoding="utf-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item 
        android:state_selected="true"
        android:drawable="@color/pressed_color"/>
    <item
        android:drawable="@color/default_color" />
</selector>
  1. MainActivity.java

package com.example.listtester;

import java.util.ArrayList;
import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity
{
 private RecyclerView    mRecyclerView;
 private MyAdapter     mAdapter;
 private RecyclerView.LayoutManager mLayoutManager;

 public static class ViewHolder extends RecyclerView.ViewHolder
 {
  // each data item is just a string in this case
  public TextView txtHeader;
  public TextView txtFooter;

  public ViewHolder( View v )
  {
   super( v );
   txtHeader = (TextView)v.findViewById( R.id.firstLine );
   txtFooter = (TextView)v.findViewById( R.id.secondLine );
  }
 }

 @Override
 protected void onCreate( Bundle savedInstanceState )
 {
  super.onCreate( savedInstanceState );
  setContentView( R.layout.activity_main );
  mRecyclerView = (RecyclerView)findViewById( R.id.my_recycler_view );

  // use this setting to improve performance if you know that changes
  // in content do not change the layout size of the RecyclerView
  mRecyclerView.setHasFixedSize( true );

  // use a linear layout manager
  mLayoutManager = new LinearLayoutManager( this );
  mRecyclerView.setLayoutManager( mLayoutManager );

  // specify an adapter (see also next example)
  ArrayList<String> myDataset = new ArrayList<String>();
  myDataset.add( "1234" );
  myDataset.add( "adsf" );
  myDataset.add( "2344" );
  myDataset.add( "1234" );
  myDataset.add( "32456344" );
  myDataset.add( "12356564" );
  myDataset.add( "1dfsdg234" );
  myDataset.add( "1234" );
  myDataset.add( "32456344" );
  myDataset.add( "12356564" );
  myDataset.add( "1dfsdg234" );
  myDataset.add( "1234" );
  myDataset.add( "32456344" );
  myDataset.add( "12356564" );
  myDataset.add( "1dfsdg234" );
  mAdapter = new MyAdapter( myDataset );
  mRecyclerView.setAdapter( mAdapter );

  Button up = (Button)findViewById( R.id.up_button );
  up.setOnClickListener( new OnClickListener()
  {
   @Override
   public void onClick( View v )
   {
    mAdapter.scrollUp();
   }
  } );

  Button down = (Button)findViewById( R.id.down_button );
  down.setOnClickListener( new OnClickListener()
  {
   @Override
   public void onClick( View v )
   {
    mAdapter.scrollDown();
   }
  } );
 }

 @Override
 public boolean onCreateOptionsMenu( Menu menu )
 {
  // Inflate the menu; this adds items to the action bar if it is present.
  getMenuInflater().inflate( R.menu.main, menu );
  return true;
 }

 @Override
 public boolean onOptionsItemSelected( MenuItem item )
 {
  // Handle action bar item clicks here. The action bar will
  // automatically handle clicks on the Home/Up button, so long
  // as you specify a parent activity in AndroidManifest.xml.
  int id = item.getItemId();
  if( id == R.id.action_settings )
  {
   return true;
  }
  return super.onOptionsItemSelected( item );
 }
}
  1. MyAdapter.java

package com.example.listtester;

import java.util.ArrayList;
import android.support.v7.widget.RecyclerView;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.TextView;

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>
{
 private ArrayList<String> mDataset;
 private int     mSelectedItem = 0;
 private RecyclerView  mRecyclerView;

 // 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 class ViewHolder extends RecyclerView.ViewHolder
 {
  // each data item is just a string in this case
  public TextView txtHeader;
  public TextView txtFooter;

  public ViewHolder( View v )
  {
   super( v );
   txtHeader = (TextView)v.findViewById( R.id.firstLine );
   txtFooter = (TextView)v.findViewById( R.id.secondLine );

   // Handle item click and set the selection
   itemView.setClickable( true );
   itemView.setOnClickListener( new View.OnClickListener()
   {
    @Override
    public void onClick( View v )
    {
     // Redraw the old selection and the new
     notifyItemChanged( mSelectedItem );
     mSelectedItem = mRecyclerView.getChildAdapterPosition( v );
     notifyItemChanged( mSelectedItem );
    }
   } );
  }
 }
 
 public void scrollUp()
 {
  RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
  tryMoveSelection( lm, -1 );
 }
 
 public void scrollDown()
 {
  RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
  tryMoveSelection( lm, 1 );
 }

 @Override
 public void onAttachedToRecyclerView( final RecyclerView recyclerView )
 {
  super.onAttachedToRecyclerView( recyclerView );
  mRecyclerView = recyclerView;
 }

 private boolean tryMoveSelection( RecyclerView.LayoutManager lm, int direction )
 {
  int nextSelectItem = mSelectedItem + direction;

  // If still within valid bounds, move the selection, notify to redraw,
  // and scroll
  if( nextSelectItem >= 0 && nextSelectItem < getItemCount() )
  {
   notifyItemChanged( mSelectedItem );
   mSelectedItem = nextSelectItem;
   notifyItemChanged( mSelectedItem );
   lm.scrollToPosition( mSelectedItem );
   return true;
  }

  return false;
 }

 public void add( int position, String item )
 {
  mDataset.add( position, item );
  notifyItemInserted( position );
 }

 public void remove( String item )
 {
  int position = mDataset.indexOf( item );
  mDataset.remove( position );
  notifyItemRemoved( position );
 }

 // Provide a suitable constructor (depends on the kind of dataset)
 public MyAdapter( ArrayList<String> myDataset )
 {
  mDataset = myDataset;
 }

 // Create new views (invoked by the layout manager)
 @Override
 public MyAdapter.ViewHolder onCreateViewHolder( ViewGroup parent,
   int viewType )
 {
  // create a new view
  View v = LayoutInflater.from( parent.getContext() ).inflate(
    R.layout.item_layout, parent, false );
  // set the view's size, margins, paddings and layout parameters
  ViewHolder vh = new ViewHolder( v );
  return vh;
 }

 // Replace the contents of a view (invoked by the layout manager)
 @Override
 public void onBindViewHolder( ViewHolder holder, int position )
 {
  // - get element from your dataset at this position
  // - replace the contents of the view with that element
  final String name = mDataset.get( position );
  holder.txtHeader.setText( mDataset.get( position ) );
//  holder.txtHeader.setOnClickListener( new OnClickListener()
//  {
//   @Override
//   public void onClick( View v )
//   {
//    remove( name );
//   }
//  } );

  holder.txtFooter.setText( "Footer: " + mDataset.get( position ) );
  holder.itemView.setSelected( mSelectedItem == position );

 }

 // Return the size of your dataset (invoked by the layout manager)
 @Override
 public int getItemCount()
 {
  return mDataset.size();
 }

}

I had gone to such a long and frustrating jurney just to achieve this very basic selection thingy (not to mention adding the recyclerview to the project...) Hope it helps someone.

rubmz
  • 1,947
  • 5
  • 27
  • 49
1

If I understand right, you want to scroll to the correct position. The problem of getting the correct row position (in ListView) while scrolling has an inherent timing issue. At any point in time, we need to determine which row positions are visible. I have used the listener setOnScrollListener. Sample code:

listview.setOnScrollListener(new AbsListView.OnScrollListener() {
   @Override
   public void onScrollStateChanged(AbsListView view, int scrollState) {
                }

   @Override
   public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
      topVisiblePosition = firstVisibleItem;
      ...
   }

Later on...

listview.setSelection(topVisiblePosition);

Notes:

  • I do not notice any performance lag with onScroll listener method.
  • The setSelection method is sufficient for me.
  • Variable topVisiblePosition is saved in the class, used with setSelection method.
  • onScroll override method has useful information like visibleItemCount and its total count, good for determining the last visible row at least.
The Original Android
  • 6,147
  • 3
  • 26
  • 31
  • Interesting answer... scroll - then set selection. Will check tomorrow. Thanks! – rubmz Apr 08 '15 at 23:07
  • Found it problematic. But hey, it's listview that's just not built for it... Not with any simple/reasonable solution. – rubmz Apr 09 '15 at 22:07