1. 程式人生 > >Android檢視的頂部懸停的實現

Android檢視的頂部懸停的實現

何為檢視的頂部懸停呢?上圖看吧:

我這裡給出2中實現方式:


一,用ScrollView+listView實現。

問題的思考:

a,如何知道ScrollView滑動到了什麼位置(即我怎麼知道我的Y軸滑動到了哪裡?)

我在網上發現了這麼一個類ObservableScrollView,我不知道出處了,不好意思哈。他是重寫ScrollView,在ScrollView的onScrollChange設定了一個介面回撥,把滑動的那個Y軸通過介面傳出去。

public class ObservableScrollView extends ScrollView {

    public ObservableScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // TODO Auto-generated constructor stub
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        // TODO Auto-generated method stub
        super.onScrollChanged(l, t, oldl, oldt);
        if (mCallbacks != null) {
            mCallbacks.onScrollChanged(t);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // TODO Auto-generated method stub
        if (mCallbacks != null) {
            switch (ev.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    mCallbacks.onDownMotionEvent();
                    break;

                case MotionEvent.ACTION_UP:
                    break;
                case MotionEvent.ACTION_CANCEL:
                    mCallbacks.onUpOrCancelMotionEvent();
                    break;
            }
        }
        return super.onTouchEvent(ev);
    }


    @Override
    protected int computeVerticalScrollRange() {
        // TODO Auto-generated method stub
        return super.computeVerticalScrollRange();
    }

    public static interface Callbacks {
        public void onScrollChanged(int scrollY);

        public void onDownMotionEvent();

        public void onUpOrCancelMotionEvent();
    }

    private Callbacks mCallbacks;

    public void setCallbacks(Callbacks listener) {
        mCallbacks = listener;
    }

}

b,懸停的動作怎麼實現

懸停的動作當然是通過平移動畫實現啦。

 @Override
    public void onScrollChanged(int scrollY) {
        stickyView.setTranslationY(Math.max(stopView.getTop() - titleViewHeight, scrollY));
        titleView.setBackgroundColor(ColorUtil.getNewColorByStartEndColor(this, (float)((scrollY * 1.0 / (stopView.getTop())) > 1 ? 1 : (scrollY * 1.0 / (stopView.getTop()))), R.color.colorTransparent, R.color.colorWhite));
    }
c,標題欄顏色漸變的怎麼實現
// 成新的顏色值
    public static int getNewColorByStartEndColor(Context context, float fraction, int startValue, int endValue) {
        return evaluate(fraction, context.getResources().getColor(startValue), context.getResources().getColor(endValue));
    }
    /**
     * 成新的顏色值
     * @param fraction 顏色取值的級別 (0.0f ~ 1.0f)
     * @param startValue 開始顯示的顏色
     * @param endValue 結束顯示的顏色
     * @return 返回生成新的顏色值
     */
    public static int evaluate(float fraction, int startValue, int endValue) {
        int startA = (startValue >> 24) & 0xff;
        int startR = (startValue >> 16) & 0xff;
        int startG = (startValue >> 8) & 0xff;
        int startB = startValue & 0xff;

        int endA = (endValue >> 24) & 0xff;
        int endR = (endValue >> 16) & 0xff;
        int endG = (endValue >> 8) & 0xff;
        int endB = endValue & 0xff;

        return ((startA + (int) (fraction * (endA - startA))) << 24) |
                ((startR + (int) (fraction * (endR - startR))) << 16) |
                ((startG + (int) (fraction * (endG - startG))) << 8) |
                ((startB + (int) (fraction * (endB - startB))));
    }
titleView.setBackgroundColor(ColorUtil.getNewColorByStartEndColor(this, (float)((scrollY * 1.0 / (stopView.getTop())) > 1 ? 1 : (scrollY * 1.0 / (stopView.getTop()))), R.color.colorTransparent, R.color.colorWhite));
通過scrollY與懸停View的初始位置的比值計算出透明度的值。

d,ListView插入ScrollView的正確方式

之所以說正確方式,首先我們看看錯誤的方法,我直接把ListView插入ScrollView


我相信很多同學都遇到同樣的問題,不只是ListView,GridView也是一樣的,都會出問題。我在網上了百度了一下,找到了2個解決辦法,這裡我貼出一種解決辦法:

public class MyXListView extends ListView {
    public MyXListView(Context context) {
        this(context,null);
    }

    public MyXListView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

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

    /**
     * 重寫該方法,達到使ListView適應ScrollView的效果
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

}

該方法也同樣適用於GridView.好!看一下我把ListView改成MyXListView之後的效果:


一切都還好就是螢幕顯示有點問題,為什麼自動滑到了ListView檢視上而不是ScrollView的頂部呢?既然它自動滑到了ListView上,那我就用2行程式碼把它滑動上去吧!

   observableScrollView.scrollTo(0, 0);
        observableScrollView.smoothScrollTo(0, 0);//設定scrollView預設滾動到頂部

這樣看起來就是我想要的效果了。對吧!最後一個問題思考

e,這樣效率怎麼樣?

相信細心的人已經看出來了,我每次進入的時候會有一會兒的白屏,那會兒應該是在繪製檢視,我只是在LIstView載入100個item額。來我們繪製1000個試試。


已經很明顯了。

為什麼會出現這種情況呢?原因是我重寫了ListView一次就載入所有的item,從而導致了item根本沒有重用。那為什麼要一次性載入所有的item呢?還不是為了知道ListView的高度,由於ScrollView鑲嵌ListView導致無法正確的測繪出ListView的高度,我們需要提前讓ScrollView知道ListView的高度,好正確的顯示ListView.所以這種方法顯示小量的資料還可以,大量的資料就顯得無力了。

二,用ListView增加頭部實現。

ListView的實現我是在StickyHeaderListView的基礎上進行改進的,StickyHeaderListView的原文地址:http://www.open-open.com/lib/view/open1461744699190.html

先來看看效果:


我改了2個地方:

1,補空檢視

StickyHeaderListView的那個補空檢視是先設定一個ONE_SCREEN_COUNT(一屏能顯示的個數,這個根據螢幕高度和各自的需求定),然後用下面程式碼新增資料:

  // 設定資料
    public void setData(List<TravelingEntity> list) {
        clearAll();
        addALL(list);

        isNoData = false;
        if (list.size() == 1 && list.get(0).isNoData()) {
            // 暫無資料佈局
            isNoData = list.get(0).isNoData();
            mHeight = list.get(0).getHeight();
        } else {
            // 新增空資料
            if (list.size() < ONE_SCREEN_COUNT) {
                addALL(createEmptyList(ONE_SCREEN_COUNT - list.size()));
            }
        }
        notifyDataSetChanged();
    }
可以看到當list.size()<ONE_SCREEN_COUNT時就開始新增空資料,新增空資料的個數為ONE_SCREEN_COUNT-list.size()。

然後我們看看它的getView()程式碼:

 @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // 暫無資料
        if (isNoData) {
            convertView = mInflater.inflate(R.layout.item_no_data_layout, null);
            AbsListView.LayoutParams params = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, mHeight);
            RelativeLayout rootView = ButterKnife.findById(convertView, R.id.rl_root_view);
            rootView.setLayoutParams(params);
            return convertView;
        }

        // 正常資料
        final ViewHolder holder;
        if (convertView != null && convertView instanceof LinearLayout) {
            holder = (ViewHolder) convertView.getTag();
        } else {
            convertView = mInflater.inflate(R.layout.item_travel, null);
            holder = new ViewHolder(convertView);
            convertView.setTag(holder);
        }

        TravelingEntity entity = getItem(position);

        holder.llRootView.setVisibility(View.VISIBLE);
        if (TextUtils.isEmpty(entity.getType())) {
            holder.llRootView.setVisibility(View.INVISIBLE);
            return convertView;
        }

        holder.tvTitle.setText(entity.getFrom() + entity.getTitle() + entity.getType());
        holder.tvRank.setText("排名:" + entity.getRank());
        mImageManager.loadUrlImage(entity.getImage_url(), holder.ivImage);

        return convertView;
    }

可以看到當他載入到空資料的時候採用了holder.llRootView.setVisibility(View.INVISIBLE);來達到補空檢視將資料佔滿整個螢幕的效果。而空檢視的個數為ONE_SCREEN_COUNT-list.size(),空檢視和正常檢視效果一樣,只是顯示為VIEW.INVISIBLE。而ONE_SCREEN_COUNT很難控制,所以空檢視的個數也就很難控制,這樣就造成整個空檢視過高,而可以把正常資料滑出螢幕。


既然找到原因,程式碼優化:

/**
     * 給適配填充資料來源,用來顯示資料
     *
     * @param list
     */
    public void setData(List<T> list) {
        isNoData = false;
        clearAll();
        addAll(list);

        //當可用資料少於設定的一屏資料時新增空資料將顯示撐滿一屏
        if (mList.size() < ONE_SCREEN_COUNT) {
            addAll(addEmptyData(1));
        }
        notifyDataSetChanged();
    }
我的想法是隻要list.size()<ONE_SCREEN_COUNT,就新增一個空檢視,空檢視不與正常資料公用一個檢視,而是自己獨立的一個檢視,空檢視的高度通過計算獲得。這樣ONE_SCREEN_COUNT的數值就不需要特別準備,寫大點準沒錯(當然我不推薦,自己估摸著來,不需要特別準確)

那麼空檢視的高度怎麼計算呢?其實也很簡單的:

switch (type) {
                //這個檢視有可能是空檢視,也有可能是補空檢視(就是有資料,但是不足以佔滿整個螢幕時,增加的檢視將其佔滿整個檢視)
                //當getCount()為1,就是空檢視,getCount()>1就是補空檢視
                case VIEW_TYPE_2:
                    AbsListView.LayoutParams params = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                            (AndroidUtils.getScreenSize()[1] - getItemViewHeigh(parent) * (getCount() - 1) - mHeight));
                    convertView.setLayoutParams(params);
                    if (getCount() != 1) {//標識有填充空資料
                        convertView.setVisibility(View.INVISIBLE);
                    } else {//就只有一個空檢視
                        convertView.setVisibility(View.VISIBLE);
                    }
                    break;
            }
這裡有一個非常重要的方法要提一下就是獲取listview的item的高度的方法:
/**
     * 獲取ListViewItem的高度
     *
     * @param parent
     * @return
     */
    private int getItemViewHeigh(ViewGroup parent) {
        View view = getView(0, null, parent);
        view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
        return view.getMeasuredHeight();
    }

總的思想就是獲取螢幕的高度,然後測量ListView的item的高度。然後用螢幕的高度減去所有正常資料的item的高度就是空檢視所需要的高度。

2.介面回撥

StickyHeaderListView中的懸浮檢視的真檢視(真正的懸浮檢視,響應點選事件的View)和假檢視(只做顯示用View)之間用了很多介面回撥來傳遞點選事件,看得人比較頭暈,這裡我將它們全部替換成了RxBus,詳情可以檢視專案

總結:ScrollView+ListView實現頂部懸停程式碼比較簡單,但是ScrollView+ListView的巢狀導致了ListView的item重用機制失效,所以在處理大量資料顯示的時候會大大的降低效率。ListView增加頭部實現程式碼邏輯比較複雜,但是它保留了ListView的item重用的機制,在處理大量資料顯示的時候效率也沒有降低。所以在資料小而且簡單功能的時候ScrollView+ListView會比較實用,但是資料量大而且功能複雜我還是推薦實用ListView增加頭部實現懸停。

專案地址:http://download.csdn.net/detail/baidu_34012226/9620653