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,懸停的動作怎麼實現
懸停的動作當然是通過平移動畫實現啦。
c,標題欄顏色漸變的怎麼實現@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)); }
// 成新的顏色值 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