Thursday 7 April 2011

Make Horizontal List View Withs Button in Android

In Android API seems to be lacking a Horizontal ListView widget. Basically, what I needed was something exactly like the Gallery widget but without the center-locking feature.



My Android Horizontal ListView implementation has the following features:
  • Subclass AdapterView so I can make use of adapters
  • Fast – make use of recycled views when possible
  • Items are clickable – (accepts AdapterView.OnItemClickListener)
  • Scrollable
  • No center-locking
  • Simple – is that so much to ask?       
package com.android.horizontal;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.Toast;

public class HorizontalListViewDemo extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.listviewdemo);

HorizontialListView listview = (HorizontialListView) findViewById(R.id.listview);
listview.setAdapter(mAdapter);

}

private static String[] dataObjects = new String[] { "Button#1",
"Button#2", "Button#3", "Button#4", "Button#5", "Button#6",
"Button#7", "Button#8" };

private BaseAdapter mAdapter = new BaseAdapter() {

@Override
public int getCount() {
return dataObjects.length;
}

@Override
public Object getItem(int position) {
return null;
}

@Override
public long getItemId(int position) {
return 0;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
final int pos = position;
View retval = LayoutInflater.from(parent.getContext()).inflate(
R.layout.viewitem, null);
// TextView title = (TextView) retval.findViewById(R.id.title);
// title.setText(dataObjects[position]);
final Button bt = (Button) retval.findViewById(R.id.btview);
bt.setText(dataObjects[position]);
bt.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
// TODO Auto-generated method stub
// String btname;

Log.v("log_tag", "here the position of it");
Toast.makeText(HorizontalListViewDemo.this,
"button is clicked", Toast.LENGTH_SHORT).show();
Log.v("log_tag", "bt clicked");
}
});

return retval;

}

};

}
-----------------------------------------------------------------------------------------------------------
here is other class for Horizontal List view

/*
 * HorizontalListView.java
 *
 * 
 * The MIT License
 * Copyright (c) 2011 Paul Soucy (paul@dev-smart.com)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

package com.android.horizontal;

import java.util.LinkedList;
import java.util.Queue;

import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.Scroller;

public class HorizontialListView extends AdapterView<ListAdapter> {

public boolean mAlwaysOverrideTouch = true;
protected ListAdapter mAdapter;
private int mLeftViewIndex = -1;
private int mRightViewIndex = 0;
protected int mCurrentX;
protected int mNextX;
private int mMaxX = Integer.MAX_VALUE;
private int mDisplayOffset = 0;
protected Scroller mScroller;
private GestureDetector mGesture;
private Queue<View> mRemovedViewQueue = new LinkedList<View>();
private OnItemSelectedListener mOnItemSelected;
private OnItemClickListener mOnItemClicked;

public HorizontialListView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
mLeftViewIndex = -1;
mRightViewIndex = 0;
mDisplayOffset = 0;
mCurrentX = 0;
mNextX = 0;
mMaxX = Integer.MAX_VALUE;
mScroller = new Scroller(getContext());
mGesture = new GestureDetector(getContext(), mOnGesture);
}
@Override
public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) {
mOnItemSelected = listener;
Log.v("log_tag", "Message is set On Clicked");
}
@Override
public void setOnItemClickListener(AdapterView.OnItemClickListener listener){
mOnItemClicked = listener;
Log.v("log_tag", "Set on Item Clicked");
}
private DataSetObserver mDataObserver = new DataSetObserver() {

@Override
public void onChanged() {
// TODO Auto-generated method stub
super.onChanged();
}

@Override
public void onInvalidated() {
// TODO Auto-generated method stub
super.onInvalidated();
}
};

@Override
public ListAdapter getAdapter() {
return mAdapter;
}

@Override
public View getSelectedView() {
//TODO: implement
return null;
}

@Override
public void setAdapter(ListAdapter adapter) {
if(mAdapter != null) {
mAdapter.unregisterDataSetObserver(mDataObserver);
}
mAdapter = adapter;
mAdapter.registerDataSetObserver(mDataObserver);
initView();
removeAllViewsInLayout();
        requestLayout();
}

@Override
public void setSelection(int position) {
//TODO: implement
}
private void addAndMeasureChild(final View child, int viewPos) {
LayoutParams params = child.getLayoutParams();
if(params == null) {
params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
}

addViewInLayout(child, viewPos, params, true);
child.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
}

@Override
protected synchronized void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);

if(mAdapter == null){
return;
}

if(mScroller.computeScrollOffset()){
int scrollx = mScroller.getCurrX();
mNextX = scrollx;
}
if(mNextX < 0){
mNextX = 0;
mScroller.forceFinished(true);
}
if(mNextX > mMaxX) {
mNextX = mMaxX;
mScroller.forceFinished(true);
}
int dx = mCurrentX - mNextX;
removeNonVisibleItems(dx);
fillList(dx);
positionItems(dx);
mCurrentX = mNextX;
if(!mScroller.isFinished()){
post(new Runnable(){
@Override
public void run() {
requestLayout();
}
});
}
}
private void fillList(final int dx) {
int edge = 0;
View child = getChildAt(getChildCount()-1);
if(child != null) {
edge = child.getRight();
}
fillListRight(edge, dx);
edge = 0;
child = getChildAt(0);
if(child != null) {
edge = child.getLeft();
}
fillListLeft(edge, dx);
}
private void fillListRight(int rightEdge, final int dx) {
while(rightEdge + dx < getWidth() && mRightViewIndex < mAdapter.getCount()) {
View child = mAdapter.getView(mRightViewIndex, mRemovedViewQueue.poll(), this);
addAndMeasureChild(child, -1);
rightEdge += child.getMeasuredWidth();
if(mRightViewIndex == mAdapter.getCount()-1){
mMaxX = mCurrentX + rightEdge - getWidth();
}
mRightViewIndex++;
}
}
private void fillListLeft(int leftEdge, final int dx) {
while(leftEdge + dx > 0 && mLeftViewIndex >= 0) {
View child = mAdapter.getView(mLeftViewIndex, mRemovedViewQueue.poll(), this);
addAndMeasureChild(child, 0);
leftEdge -= child.getMeasuredWidth();
mLeftViewIndex--;
mDisplayOffset -= child.getMeasuredWidth();
}
}
private void removeNonVisibleItems(final int dx) {
View child = getChildAt(0);
while(child != null && child.getRight() + dx <= 0) {
mDisplayOffset += child.getMeasuredWidth();
mRemovedViewQueue.offer(child);
removeViewInLayout(child);
mLeftViewIndex++;
child = getChildAt(0);
}
child = getChildAt(getChildCount()-1);
while(child != null && child.getLeft() + dx >= getWidth()) {
mRemovedViewQueue.offer(child);
removeViewInLayout(child);
mRightViewIndex--;
child = getChildAt(getChildCount()-1);
}
}
private void positionItems(final int dx) {
if(getChildCount() > 0){
mDisplayOffset += dx;
int left = mDisplayOffset;
for(int i=0;i<getChildCount();i++){
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
child.layout(left, 0, left + childWidth, child.getMeasuredHeight());
left += childWidth;
}
}
}
public synchronized void scrollTo(int x) {
mScroller.startScroll(mNextX, 0, x - mNextX, 0);
requestLayout();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = mGesture.onTouchEvent(ev);
return handled;
}
protected boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
synchronized(HorizontialListView.this){
mScroller.fling(mNextX, 0, (int)-velocityX, 0, 0, mMaxX, 0, 0);
}
requestLayout();
return true;
}
protected boolean onDown(MotionEvent e) {
mScroller.forceFinished(true);
return true;
}
private OnGestureListener mOnGesture = new GestureDetector.SimpleOnGestureListener() {

@Override
public boolean onDown(MotionEvent e) {
return HorizontialListView.this.onDown(e);
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
return HorizontialListView.this.onFling(e1, e2, velocityX, velocityY);
}

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
synchronized(HorizontialListView.this){
mNextX += (int)distanceX;
}
requestLayout();
return true;
}

@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Rect viewRect = new Rect();
for(int i=0;i<getChildCount();i++){
View child = getChildAt(i);
int left = child.getLeft();
int right = child.getRight();
int top = child.getTop();
int bottom = child.getBottom();
viewRect.set(left, top, right, bottom);
if(viewRect.contains((int)e.getX(), (int)e.getY())){
if(mOnItemClicked != null){
mOnItemClicked.onItemClick(HorizontialListView.this, child, mLeftViewIndex + 1 + i, 0);
}
if(mOnItemSelected != null){
mOnItemSelected.onItemSelected(HorizontialListView.this, child, mLeftViewIndex + 1 + i, 0);
}
break;
}
}
return true;
}
};


}
-----------------------------------------------------------------------------------------------------
this is main xml file 

<?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:background="#fff"
  >
  
  <com.android.horizontal.HorizontialListView  
  android:id="@+id/listview"
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"
   android:background="#ddd"
  />
  
</LinearLayout>

--------------------------------------------------------------------------
here is the xml file for custom list for horizontal view

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:background="#fff"
  >
  
  <Button
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:id="@+id/btview"
  android:background="@android:drawable/btn_default"
  />
  
</LinearLayout>
-----------------------------------------------------------------------------
If any problem share with me by make comments here .




17 comments:

  1. Thanks for the code, it is great!!

    Only one thing. I tried your code and it works fine, but I can not click on a button directly. First I have to selected it and click it with phone buttons, never when I touch the screen. How can it be fixed?

    Thanks a lot

    ReplyDelete
  2. i tried this one and it works ok. problem is i cannot add another widget, say , even a TextView below that horizontal list view.

    seems the horizontal list view occupies all the height of the screen even if layout_height is set to wrap_content

    ReplyDelete
  3. Great example!
    I tried it but having some errors. No response on the click listener.
    I got the following error from my main.xml file.

    "The following classes could not be instantiated:
    - com.bach.kika.HorizontialListView
    See the Error Log (Window > Show View) for more details.
    Tip: Use View.isInEditMode() in your custom views to skip code when shown in Eclipse "

    Any idea on how to fix it?
    It would be cool if u post the entire project source code!

    Thanks
    -E

    ReplyDelete
  4. Agreed to Ravi..
    its not allowing for click listerner

    ReplyDelete
  5. Agreed to salik and ravi... for the click listener

    ReplyDelete
  6. A response to the click listener problem would be nice...

    ReplyDelete
  7. Not working for me i am passing list to BaseAdapter and notifying adpater when adding to list

    ReplyDelete
  8. How to make the onClick event on the button to work?

    ReplyDelete
  9. Great
    Thanks for code

    ReplyDelete
  10. onclick not working? how to make it work

    ReplyDelete
  11. where is the R.layout.viewitem ? I cannot find it. Please help me!!

    ReplyDelete
  12. Thanks for the code. However can you tell how the scroll bar can be added.

    ReplyDelete
  13. @For all who have problem in getting Button Click Event,Still i have not able to do so ,please if any one have find they can updated them code here for Button Click ,i also will updated when i find appropriate solution.

    ReplyDelete
  14. hi poeple,

    the problem is the gesturelistener.

    i have solved it a little bit dirty but it work for me:

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
    Rect viewRect = new Rect();
    x=(int)e.getX();
    y=(int)e.getY();
    for(int i=0;i<getChildCount();i++){
    LinearLayout child = (LinearLayout) getChildAt(i);
    for(int ci=0;ci<child.getChildCount();ci++)
    {
    View ci_child=child.getChildAt(ci);
    ci_child.getGlobalVisibleRect(viewRect);
    if(viewRect.contains(x, y)){
    if(mOnItemClicked != null){
    mOnItemClicked.onItemClick(HorizontialListView.this, ci_child, mLeftViewIndex + 1 + i, mAdapter.getItemId( mLeftViewIndex + 1 + i ));
    }
    if(mOnItemSelected != null){
    mOnItemSelected.onItemSelected(HorizontialListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId( mLeftViewIndex + 1 + i ));
    }
    break;
    }
    else
    {
    Log.d("listview", "nomatch"+x+" "+y);
    }
    }


    }
    return true;
    }

    dont't forget:

    btn.setId(1000 + position);

    ReplyDelete
  15. Its really a very helpful tutorial. I used it and it works fine but i have one issue that its not click-able. Please assist me.

    ReplyDelete
  16. *your* implementation? it is from Paul

    ReplyDelete
  17. How to give space between items in Horizontal ListView..??plz tell me..

    ReplyDelete