1. 程式人生 > >Android -- 自定義實現橫豎雙向滾動的列表(ListView)佈局

Android -- 自定義實現橫豎雙向滾動的列表(ListView)佈局

public class CustomHScrollView extends HorizontalScrollView {

    ScrollViewObserver mScrollViewObserver = new ScrollViewObserver();

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

    public CustomHScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomHScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(ev);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        //滾動時通知觀察者
        if (mScrollViewObserver != null) {
            mScrollViewObserver.NotifyOnScrollChanged(l, t, oldl, oldt);
        }
        super.onScrollChanged(l, t, oldl, oldt);
    }

    /*
     * 當發生了滾動事件時介面,供外部訪問
     */
    public static interface OnScrollChangedListener {
        public void onScrollChanged(int l, int t, int oldl, int oldt);
    }

    /*
     * 新增滾動事件監聽
     * */
    public void AddOnScrollChangedListener(OnScrollChangedListener listener) {
        mScrollViewObserver.AddOnScrollChangedListener(listener);
    }

    /*
     * 移除滾動事件監聽
     * */
    public void RemoveOnScrollChangedListener(OnScrollChangedListener listener) {
        mScrollViewObserver.RemoveOnScrollChangedListener(listener);
    }
    /*
     * 滾動觀察者
     */
    public static class ScrollViewObserver {
        List<OnScrollChangedListener> mChangedListeners;

        public ScrollViewObserver() {
            super();
            mChangedListeners = new ArrayList<OnScrollChangedListener>();
        }
        //新增滾動事件監聽
        public void AddOnScrollChangedListener(OnScrollChangedListener listener) {
            mChangedListeners.add(listener);
        }
        //移除滾動事件監聽
        public void RemoveOnScrollChangedListener(OnScrollChangedListener listener) {
            mChangedListeners.remove(listener);
        }
        //通知
        public void NotifyOnScrollChanged(int l, int t, int oldl, int oldt) {
            if (mChangedListeners == null || mChangedListeners.size() == 0) {
                return;
            }
            for (int i = 0; i < mChangedListeners.size(); i++) {
                if (mChangedListeners.get(i) != null) {
                    mChangedListeners.get(i).onScrollChanged(l, t, oldl, oldt);
                }
            }
        }
    }
}

實現圖中的效果最重要的是如何讓資料行與標題行保持同時同向滑動,由上面的程式碼可以看出,我定義了一個滾動觀察者,用於監聽滾動事件,每次滾動事件的觸發(包括標題頭和資料行每一行的滾動)都會通知給觀察者,之後觀察者再通知給它的訂閱者,保持滾動的一致性。程式碼中都有註釋,一目瞭然。 3、出此之外還要自定義一個攔截onTouch事件的佈局
public class InterceptScrollLinearLayout extends LinearLayout {

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

    public InterceptScrollLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public InterceptScrollLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }
}

關鍵程式碼就是上面這兩個自定義類 來看MainActivity,只貼部分關鍵程式碼
public class MainActivity extends AppCompatActivity {

    private RelativeLayout mHead;//標題頭
    private ListView mListView;
    private List<TestData> mDataList;
    private ListViewAdapter mAdapter;
    CustomHScrollView mScrollView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        initView();
        initData();
    }

    private void initView(){
        mListView = (ListView) findViewById(R.id.list_view);
        mScrollView = (CustomHScrollView) findViewById(R.id.h_scrollView);
        mHead = (RelativeLayout) findViewById(R.id.head_layout);
        mHead.setBackgroundResource(R.color.colorAccent);
        mHead.setFocusable(true);
        mHead.setClickable(true);
        mHead.setOnTouchListener(new MyTouchLinstener());
        mListView.setOnTouchListener(new MyTouchLinstener());
    }

    /**
     * 載入資料
     */
    private void initData(){
        mDataList = new ArrayList<>();
        TestData data = null;
        for (int i = 1; i <= 100; i++) {
            data = new TestData();
            data.setText1("第"+i+"行-1");
            data.setText2("第"+i+"行-2");
            data.setText3("第"+i+"行-3");
            data.setText4("第"+i+"行-4");
            data.setText5("第"+i+"行-5");
            data.setText6("第"+i+"行-6");
            data.setText7("第"+i+"行-7");
            mDataList.add(data);
        }
        setData();
    }

    private void setData(){
        mAdapter = new ListViewAdapter(this, mDataList, mHead);
        mListView.setAdapter(mAdapter);
    }

    class MyTouchLinstener implements View.OnTouchListener {

        @Override
        public boolean onTouch(View arg0, MotionEvent arg1) {
            //當在表頭和listView控制元件上touch時,將事件分發給 ScrollView
            HorizontalScrollView headSrcrollView = (HorizontalScrollView) mHead.findViewById(R.id.h_scrollView);
            headSrcrollView.onTouchEvent(arg1);
            return false;
        }
    }

    //...............
}

再看介面卡
public class ListViewAdapter extends BaseAdapter {

    private Context mContext;
    private List<TestData> mList;
    private LayoutInflater mInflater;
    private RelativeLayout mHead;

    public ListViewAdapter(Context context, List<TestData> list, RelativeLayout head) {
        this.mContext = context;
        this.mList = list;
        this.mHead = head;
        this.mInflater = LayoutInflater.from(context);
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object getItem(int i) {
        return mList.get(i);
    }

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

    @Override
    public View getView(int i, View view, ViewGroup group) {
        MyViewHolder holder = null;
        if (view == null){
            view = mInflater.inflate(R.layout.list_item, group, false);
            holder = new MyViewHolder();
            CustomHScrollView scrollView = (CustomHScrollView) view.findViewById(R.id.h_scrollView);
            holder.scrollView = scrollView;
            
//..........

            CustomHScrollView headSrcrollView = (CustomHScrollView) mHead.findViewById(R.id.h_scrollView);
            //新增滾動監聽事件
            headSrcrollView.AddOnScrollChangedListener(new OnScrollChangedListenerImp(scrollView));

            view.setTag(holder);
        }else{
            holder = (MyViewHolder) view.getTag();
        }

        //.........

        return view;
    }

    class OnScrollChangedListenerImp implements CustomHScrollView.OnScrollChangedListener {
        CustomHScrollView mScrollViewArg;

        public OnScrollChangedListenerImp(CustomHScrollView scrollViewar) {
            mScrollViewArg = scrollViewar;
        }

        @Override
        public void onScrollChanged(int l, int t, int oldl, int oldt) {
            mScrollViewArg.smoothScrollTo(l, t);
           
        }
    };

    class MyViewHolder{
        //........
    }
}

對應的xml檔案
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/content_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.hbh.cl.listviewhorizontalscrolldemo.MainActivity"
    tools:showIn="@layout/activity_main">

    <include
        android:id="@+id/head_layout"
        layout="@layout/list_item"
        />

    <ListView
        android:id="@+id/list_view"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_below="@+id/head_layout"
        android:cacheColorHint="@android:color/transparent"
        android:dividerHeight="1dp"
        android:divider="@color/gray">
    </ListView>
</RelativeLayout>
list_item.xml檔案
<?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/white"
                android:descendantFocusability="blocksDescendants"
                android:orientation="horizontal"
                android:padding="5dp" >

    <TextView
        android:id="@+id/textView_1"
        android:layout_width="80dp"
        android:layout_height="40dp"
        android:layout_alignParentLeft="true"
        android:layout_marginLeft="2dp"
        android:layout_marginRight="2dp"
        android:gravity="center"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:maxLines="2"
        android:ellipsize="end"
        android:text="標題1"
        android:textColor="@color/black"
        android:textSize="14sp" />

    <com.hbh.cl.listviewhorizontalscrolldemo.util.InterceptScrollLinearLayout
        android:id="@+id/scroollContainter"
        android:layout_width="fill_parent"
        android:layout_height="40dp"
        android:layout_alignParentRight="true"
        android:layout_toRightOf="@id/textView_1"
        android:focusable="false" >

        <com.hbh.cl.listviewhorizontalscrolldemo.util.CustomHScrollView
            android:id="@+id/h_scrollView"
            android:layout_width="fill_parent"
            android:layout_height="40dp"
            android:focusable="false"
            android:scrollbars="none" >

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:focusable="false"
                android:orientation="horizontal" >

                <LinearLayout
                    android:layout_width="80dp"
                    android:layout_height="match_parent"
                    android:gravity="center"
                    android:layout_gravity="center"
                    android:orientation="vertical" >

                    <TextView
                        android:id="@+id/textView_2"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="2dp"
                        android:layout_marginRight="2dp"
                        android:text="標題2"
                        android:textColor="@color/black"
                        android:gravity="center"
                        android:textSize="14sp"
                        android:textAppearance="?android:attr/textAppearanceMedium" />

                </LinearLayout>

               <!-- ....省略...... -->
                
            </LinearLayout>
        </com.hbh.cl.listviewhorizontalscrolldemo.util.CustomHScrollView>
    </com.hbh.cl.listviewhorizontalscrolldemo.util.InterceptScrollLinearLayout>

</RelativeLayout>
好了,程式碼基本上都貼出來了,至於其他的測試資料的類就不貼了,來,現在讓我們跑一下我們的程式碼:            

OK,非常完美,一切都顯得那麼和諧。但這都只是寫死的資料,正常情況下是要通過網路請求獲取資料,然後通過介面卡展示資料的,咱們剛才的這種情況只能算是簡單的模擬了網路請求展示資料,那麼好了,我現在要重新整理資料,重新整理當前頁面的資料,ok,咱們再增加一個重新整理按鈕,點選重新整理,更新資料。 我們在MainActivity中增加一段程式碼:
@Override
    public boolean onOptionsItemSelected(MenuItem item) {

        int id = item.getItemId();
        if (id == R.id.action_settings) {
            initData();//重新整理,重新載入資料
            if (mAdapter != null){
                mAdapter.notifyDataSetChanged();
            }
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

這裡我們重新執行initData()方法,模擬網路請求進行重新整理,然後呼叫Adapter的notifyDataSetChanged()方法來重新整理每個item的內容,好了,現在再來跑一下程式:
我擦,這是什麼鬼,點選重新整理之後,標題頭與資料行內容完全錯亂了,怎麼回事呢???這是因為在我們點選重新整理之後,Adapter呼叫notifyDataSetChanged()方法將listview中的每個item都強制重新整理,但標題頭並不是listview的一部分,所以保持原來的狀態,造成錯亂的問題。 那怎麼解決呢???很簡單,就是在我們點選重新整理的時候,讓標題頭(可滑動部分)回到初始狀態。 什麼意思呢??咱們看,當我們在不向左滑動的情況下,重新整理是不會出現任何問題的,當我們向左滑動之後,再重新整理,出現了資料錯亂的問題,那麼好了,能不能在我們重新整理的時候讓標題頭回到初始位置呢??當然可以,看程式碼 找到我們的ListViewAdapter,其中有這麼幾行程式碼:
@Override
        public void onScrollChanged(int l, int t, int oldl, int oldt) {
            mScrollViewArg.smoothScrollTo(l, t);
            if (n == 1) {//記錄滾動的起始位置,避免因重新整理資料引起錯亂
                new MainActivity().setPosData(oldl, oldt);
            }
            n++;
        }
這個方法就是在我們左右滾動時所執行的,我們加了一個標記,記錄下第一次滾動時的位置oldl和oldt MainActivity中對應的改動:
 @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        int id = item.getItemId();
        if (id == R.id.action_settings) {
            initData();//重新整理,重新載入資料
            mScrollView.smoothScrollTo(leftPos, topPos);//每次重新整理資料都讓CustomHScrollView回到初始位置,避免錯亂
            if (mAdapter != null){
                mAdapter.notifyDataSetChanged();
            }
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    /**
     * 記錄CustomHScrollView的初始位置
     * @param l
     * @param t
     */
    public void setPosData(int l, int t){
        this.leftPos = l;
        this.topPos = t;
    }
我們看到,在重新整理的方法中我們增加了一行程式碼,讓CustomHScrollView回到初始位置,好了,我們現在再來執行一下程式:
         

OK,非常完美,一切又都是那麼和諧,但是……已經沒有問題了!!! 好了,囉囉嗦嗦那麼多,可能讓你也覺得不明不白,還不如直接看原始碼來的實在。那好,原始碼雙手奉上 原始碼傳送門 如果覺得還不錯,順手給個贊,star一下吧微笑