9

I'm Trying to make a Circular ListView with List Items arranged on Half Circle. it should look something like this:

enter image description here

There was a related post but it was closed .

I made my own Circular Custom ListView and it works fine but my Problem is that i can't arrange List Items Half Circle way as it is shown on the image. I tried several things but it was useless, I don't know how to do it.

Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
Jilberta
  • 2,836
  • 5
  • 30
  • 44

4 Answers4

19

So when I made the sample app to demo this I had to do 2 things.

First, was edit the onDraw(Canvas) on my custom view. This could be any view, I make it a TextView for simplicity. This allows me to push the view over based on an equation.

public class MyView extends TextView {

    private static final int MAX_INDENT = 300;
    private static final String TAG = MyView.class.getSimpleName();

    public MyView(Context context) {
        super(context);
    }

    public void onDraw(Canvas canvas){
        canvas.save();
        float indent = getIndent(getY());
        canvas.translate(indent, 0);
        super.onDraw(canvas);
        canvas.restore();
    }

    public float getIndent(float distance){
        float x_vertex = MAX_INDENT;
        DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
        float y_vertex = displayMetrics.heightPixels / 2 / displayMetrics.density;
        double a = ( 0 - x_vertex ) / ( Math.pow(( 0 - y_vertex), 2) ) ;
        float indent = (float) (a * Math.pow((distance - y_vertex), 2) + x_vertex);
        return indent;
    }
}

The second thing I had to do was to Override the ListView class, make it implement OnScrollListener and call setOnScrollListener(this);. Now I am able to scroll through the list, it follows the equation that I put in the view.

public class HalfCircleListView extends ListView implements AbsListView.OnScrollListener {
    public HalfCircleListView(Context context) {
        super(context);
        setOnScrollListener(this);
    }


    @Override
    public void onScrollStateChanged(AbsListView absListView, int i) {
        //Ignored
    }

    @Override
    public void onScroll(AbsListView absListView, int i, int i2, int i3) {
        absListView.invalidateViews();
    }
}

You can download the full source from my Gist.

Initial State Initial State of the View Scrolled State Scrolled to the bottom

As you can see my math is a little off... I use a parabola vs a circle, so that will have to be changed.

Ethan
  • 6,883
  • 3
  • 33
  • 41
1

You could increase / decrease the left margin for every view returned in your adapter's getView(). So, for example, for the first half of your views you increase the margin for every item by, say, 20 pixel (int margin = index * 20), and decrease it accordingly for the second half of views.

This needs a lot of fine-tuning to really look like a circular list, of course, but I think you get the idea.

TomTasche
  • 5,448
  • 7
  • 41
  • 67
  • yeah i already tried to do this way, but there is a problem. When the adapter uses getView at first it creates a view for an item but then it uses view from previously used views and you can't say which one it will use, so it looks messed up. – Jilberta May 03 '13 at 10:50
  • @Jilberta, in how far is that a problem? You set the margin according to the position passed to getView() every time the method is executed. Show us your code and we might be able to fix your problem. – TomTasche May 03 '13 at 11:29
  • @Jilberta, you're right that the listview you're extending from recycles views so they might look "messed up" if reused in certain ways, but remember you can also set a http://developer.android.com/reference/android/widget/AbsListView.html#setRecyclerListener(android.widget.AbsListView.RecyclerListener) to clear things up if they're causing confusion. Best. – Tom May 04 '13 at 17:22
1

I would try to use an existing radial menu widget if possible. This one claims to support a semi-circular radial menu. I'm not sure if this one does or not.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
0

After some more goggling, I found a solution.
It's relatively optimized and configurable like a normal ListView.

Here is main code snippets:

CircularListView.java

package com.makotokw.android.widget;

import android.annotation.TargetApi;

import android.content.Context;

import android.database.DataSetObserver;

import android.os.Build;

import android.util.AttributeSet;

import android.view.KeyEvent;

import android.view.View;

import android.view.ViewGroup;

import android.widget.AbsListView;

import android.widget.ListAdapter;

import android.widget.ListView;

public class CircularListView extends ListView implements AbsListView.OnScrollListener {

    private static final String TAG = CircularListView.class.getSimpleName();

    private static final int REPEAT_COUNT = 3;

    private int mItemHeight = 0;

    private CircularListViewListener mCircularListViewListener;

    private InfiniteListAdapter mInfiniteListAdapter;

    private boolean mEnableInfiniteScrolling = true;

    private CircularListViewContentAlignment mCircularListViewContentAlignment = CircularListViewContentAlignment.Left;

    private double mRadius = -1;

    private int mSmoothScrollDuration = 80;

    public CircularListView(Context context) {
        this(context, null);
    }

    public CircularListView(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.listViewStyle);
    }

    public CircularListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        setOnScrollListener(this);
        setClipChildren(false);
        setEnableInfiniteScrolling(true);
    }

    public void setAdapter(ListAdapter adapter) {
        mInfiniteListAdapter = new InfiniteListAdapter(adapter);
        mInfiniteListAdapter.setEnableInfiniteScrolling(mEnableInfiniteScrolling);
        super.setAdapter(mInfiniteListAdapter);
    }

    public CircularListViewListener getCircularListViewListener() {
        return mCircularListViewListener;
    }

    public void setCircularListViewListener(CircularListViewListener circularListViewListener) {
        this.mCircularListViewListener = circularListViewListener;
    }

    public void setEnableInfiniteScrolling(boolean enableInfiniteScrolling) {
        mEnableInfiniteScrolling = enableInfiniteScrolling;
        if (mInfiniteListAdapter != null) {
            mInfiniteListAdapter.setEnableInfiniteScrolling(enableInfiniteScrolling);
        }
        if (mEnableInfiniteScrolling) {
            setHorizontalScrollBarEnabled(false);
            setVerticalScrollBarEnabled(false);
        }
    }

    public CircularListViewContentAlignment getCircularListViewContentAlignment() {
        return mCircularListViewContentAlignment;
    }

    public void setCircularListViewContentAlignment(
            CircularListViewContentAlignment circularListViewContentAlignment) {
        if (mCircularListViewContentAlignment != circularListViewContentAlignment) {
            mCircularListViewContentAlignment = circularListViewContentAlignment;
            requestLayout();
        }
    }

    public double getRadius() {
        return mRadius;
    }

    public void setRadius(double radius) {
        if (this.mRadius != radius) {
            this.mRadius = radius;
            requestLayout();
        }
    }

    public int getCentralPosition() {
        double vCenterPos = getHeight() / 2.0f;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child != null) {
                if (child.getTop() <= vCenterPos
                        && child.getTop() + child.getHeight() >= vCenterPos) {
                    return getFirstVisiblePosition() + i;
                }
            }
        }
        return -1;
    }

    public View getCentralChild() {
        int pos = getCentralPosition();
        if (pos != -1) {
            return getChildAt(pos - getFirstVisiblePosition());
        }
        return null;
    }

    public void scrollFirstItemToCenter() {
        if (!mEnableInfiniteScrolling) {
            return;
        }

        int realTotalItemCount = mInfiniteListAdapter.getRealCount();
        if (realTotalItemCount > 0) {
            setSelectionFromTop(realTotalItemCount, getBaseCentralChildTop());
        }
    }

    public int getBaseCentralChildTop() {
        int itemHeight  = getItemHeight();
        if (itemHeight > 0) {
            return getHeight() / 2 - itemHeight / 2;
        }
        return 0;
    }

    public int getItemHeight() {
        if (mItemHeight == 0) {
            View child = getChildAt(0);
            if (child != null) {
                mItemHeight = child.getHeight();
            }
        }
        return mItemHeight;
    }

    public void setSelectionAndMoveToCenter(int position) {
        if (!mEnableInfiniteScrolling) {
            return;
        }

        int realTotalItemCount = mInfiniteListAdapter.getRealCount();
        if (realTotalItemCount == 0) {
            return;
        }

        position = position % realTotalItemCount;
        int centralPosition = getCentralPosition() % realTotalItemCount;

        int y = getBaseCentralChildTop();
        if (centralPosition == position) {
            View centralView = getCentralChild();
            y = centralView.getTop();
        }
        setSelectionFromTop(position + realTotalItemCount, y);
    }

    @TargetApi(Build.VERSION_CODES.FROYO)
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if (mEnableInfiniteScrolling) {
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                switch (event.getKeyCode()) {
                    case KeyEvent.KEYCODE_DPAD_UP:
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
                            smoothScrollBy(mItemHeight, mSmoothScrollDuration);
                            return true;
                        }
                        break;
                    case KeyEvent.KEYCODE_DPAD_DOWN:
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
                            smoothScrollBy(-mItemHeight, mSmoothScrollDuration);
                            return true;
                        }
                        break;
                    default:
                        break;

                }
            }
        }
        return super.dispatchKeyEvent(event);
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (scrollState == SCROLL_STATE_IDLE) {
            if (!isInTouchMode()) {
                setSelectionAndMoveToCenter(getCentralPosition());
            }
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
                         int totalItemCount) {
        if (!mEnableInfiniteScrolling) {
            return;
        }

        View itemView = this.getChildAt(0);
        if (itemView == null) {
            return;
        }

        int realTotalItemCount = mInfiniteListAdapter.getRealCount();
        if (realTotalItemCount == 0) {
            return;
        }

        if (mItemHeight == 0) {
            mItemHeight = itemView.getHeight();
        }

        if (firstVisibleItem == 0) {
            // scroll one unit
            this.setSelectionFromTop(realTotalItemCount, itemView.getTop());
        }

        if (totalItemCount == firstVisibleItem + visibleItemCount) {
            // back one unit
            this.setSelectionFromTop(firstVisibleItem - realTotalItemCount,
                    itemView.getTop());
        }

        if (mCircularListViewContentAlignment != CircularListViewContentAlignment.None) {

            double viewHalfHeight = view.getHeight() / 2.0f;

            double vRadius = view.getHeight();
            double hRadius = view.getWidth();

            double yRadius = (view.getHeight() + mItemHeight) / 2.0f;
            double xRadius = (vRadius < hRadius) ? vRadius : hRadius;
            if (mRadius > 0) {
                xRadius = mRadius;
            }

            for (int i = 0; i < visibleItemCount; i++) {
                itemView = this.getChildAt(i);
                if (itemView != null) {
                    double y = Math.abs(viewHalfHeight - (itemView.getTop() + (itemView.getHeight() / 2.0f)));
                    y = Math.min(y, yRadius);
                    double angle = Math.asin(y / yRadius);
                    double x = xRadius * Math.cos(angle);

                    if (mCircularListViewContentAlignment == CircularListViewContentAlignment.Left) {
                        x -= xRadius;
                    } else {
                        x = xRadius / 2 - x;
                    }
                    itemView.scrollTo((int) x, 0);
                }
            }
        } else {
            for (int i = 0; i < visibleItemCount; i++) {
                itemView = this.getChildAt(i);
                if (itemView != null) {
                    itemView.scrollTo(0, 0);
                }
            }
        }

        if (mCircularListViewListener != null) {
            mCircularListViewListener.onCircularLayoutFinished(this, firstVisibleItem, visibleItemCount, totalItemCount);
        }
    }

    class InfiniteListAdapter implements ListAdapter {

        private boolean mEnableInfiniteScrolling = true;

        private ListAdapter mCoreAdapter;

        public InfiniteListAdapter(ListAdapter coreAdapter) {
            mCoreAdapter = coreAdapter;
        }

        private void setEnableInfiniteScrolling(boolean enableInfiniteScrolling) {
            mEnableInfiniteScrolling = enableInfiniteScrolling;
        }

        public int getRealCount() {
            return mCoreAdapter.getCount();
        }

        public int positionToIndex(int position) {
            int count = mCoreAdapter.getCount();
            return (count == 0) ? 0 : position % count;
        }

        @Override
        public void registerDataSetObserver(DataSetObserver observer) {
            mCoreAdapter.registerDataSetObserver(observer);
        }

        @Override
        public void unregisterDataSetObserver(DataSetObserver observer) {
            mCoreAdapter.unregisterDataSetObserver(observer);
        }

        @Override
        public int getCount() {
            int count = mCoreAdapter.getCount();
            return (mEnableInfiniteScrolling) ? count * REPEAT_COUNT : count;
        }

        @Override
        public Object getItem(int position) {
            return mCoreAdapter.getItem(this.positionToIndex(position));
        }

        @Override
        public long getItemId(int position) {
            return mCoreAdapter.getItemId(this.positionToIndex(position));
        }

        @Override
        public boolean hasStableIds() {
            return mCoreAdapter.hasStableIds();
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return mCoreAdapter.getView(this.positionToIndex(position), convertView, parent);
        }

        @Override
        public int getItemViewType(int position) {
            return mCoreAdapter.getItemViewType(this.positionToIndex(position));
        }

        @Override
        public int getViewTypeCount() {
            return mCoreAdapter.getViewTypeCount();
        }

        @Override
        public boolean isEmpty() {
            return mCoreAdapter.isEmpty();
        }

        @Override
        public boolean areAllItemsEnabled() {
            return mCoreAdapter.areAllItemsEnabled();
        }

        @Override
        public boolean isEnabled(int position) {
            return mCoreAdapter.isEnabled(this.positionToIndex(position));
        }
    }
}


**CircularListViewContentAlignment.java**

package com.makotokw.android.widget;

public enum CircularListViewContentAlignment {

    None,
    Left,
    Right
}

**CircularListViewListener.java**

package com.makotokw.android.widget;

public interface CircularListViewListener {


    void onCircularLayoutFinished(CircularListView circularListView,
                                  int firstVisibleItem,
                                  int visibleItemCount,
                                  int totalItemCount);
}

For more clarification, you can also see my blog post and can comment on that. You can download sample app for eclipse.
My blog is:
http://androidpantiii.blogspot.in/2015/11/half-circular-list-view-there-were.html

Phantômaxx
  • 37,901
  • 21
  • 84
  • 115
Shivang
  • 935
  • 8
  • 18