自定義ListView實現任意View跑馬燈效果
標簽(空格分隔): 開源項目
看圖
話不多說,先來看下大圖效果吧,這里的GIF錄制有點渣,不過真實的跑出來的效果還是挺不錯的。
前言
最近項目中會加入一個新的需求,那就是把圖片和文字都實現那種跑馬燈的效果,之前想的不就是一個TextView的跑馬燈么,這個很好整的啊,并且開源的也是有這個的.這里給出這個TextView跑馬燈的開源地址.MarqueeView,但是這個并不符合我們的產品需求啊(需求如圖,整個View都要進行滾動),找了許久也沒找到自己能用的,看來只有自己去實現了。
目標想法
目標很簡單,就是只要實現這個效果,什么方式并沒有限制啊,但是過程就是比較復雜的,有時候甚至充滿了荊棘坎坷,這里想到的一種就是可不可以使用ListView,顯示幾個item通過方法去設定,然后通過一個線程來讓item進行滾動起來,并且實現循環,這樣不就是相當于實現了這個產品需求了么,想想也是哈,需求不就是這樣的么,當前可見的item是可以滾動的,而且也是循環的.哈哈看來自己的想法是可以的,接下來就看如何去實現了。
代碼實現
既然是對ListView的自定義(谷歌官方的ListView并沒有這個需求的相關函數和方法哈)
第一步:
AutoScrollListView extends ListView{
//然后重寫幾個構造方法
public AutoScrollListView(Context context, AttributeSet attrs) {
super(context, attrs);
mLoopRunnable = new LoopRunnable();
mScroller = new Scroller(context, new AccelerateInterpolator());
mInnerAdapter = new InnerAdapter();
}
public AutoScrollListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
}
第二步:
因為需要線程來控制滾動的時間,這里我們使用LoopRunnable(自定義的)
/**
* 線程管理類
*/
class LoopRunnable implements Runnable {
@Override
public void run() {
Log.i("AutoScrollListView", "run");
mAnimating = true; //線程啟動的時候設置動畫為ture
View childAt = getChildAt(0); //獲取到第一個子view
//得到滑動的高度 也就是當前可滑動的item的高度
int scrollHeight = childAt.getMeasuredHeight() + getDividerHeight();
//然后進行滑動
mScroller.startScroll(0, 0, 0, mScrollOrientation == SCROLL_UP ? scrollHeight : -scrollHeight);
invalidate(); //重新繪制
}
}
//可以看到這里使用了 private Scroller mScroller;
這里就不詳細講解為啥使用Scroller(可以實現想要的效果滑動),這里附上一篇Scroller的講解的文章 Android中滑屏實現----手把手教你如何實現觸摸滑屏以及Scroller類詳解
還有兩點,就是防止泄露內存,這個時候我們需要在View依附Window和接觸Window的時候把線程移除
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Log.i("AutoScrollListView", "onAttachedToWindow");
//發送延時消息開始線程,也就是開始View的滾動
postDelayed(mLoopRunnable, DALY_TIME);
mAnimating = true;//設置動畫
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Log.i("AutoScrollListView", "onDetachedFromWindow");
removeCallbacks(mLoopRunnable);//移除線程防止泄露內存
}
這個時候我們一個重要的問題就是怎么去測量我們的滾動視圖的高度
首先我們需要獲取到視圖的高度(因為視圖的高度我們上層并不能首先獲取到,因為我們要寫一個方法后者接口,留給使用者去實現然后后去高度),因此這個時候我們寫一個接口
public interface AutoScroll {
/**
* 返回屏幕可見個數
*
* @return 可見個數
*/
public int getVisiableCount();
/**
* 獲取條目高度
*
* @return 高度
*/
public int getListItemHeight(Context context);
}
然后在子類中去獲取到(我們的布局View的高度是可知的,也就是固定的),然后子類中如下后去(根據自己的UI需求制定的高度進行設置)
@Override
public int getListItemHeight(Context context) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, context.getResources().getDisplayMetrics());
}
然后我們通過獲取到了滾動視圖的高度之后,我們可以重寫onMeasure方法進行測量了。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mAutoScroll && mOutterAdapter != null) { //如果是自動滾動和當前的adapter不為空
AutoScroll autoScroll = (AutoScroll) mOutterAdapter; //
//獲取到高度 也就是滾動的view的高度
int height = autoScroll.getListItemHeight(getContext()) * autoScroll.getVisiableCount()
+ (autoScroll.getVisiableCount() - 1) * getDividerHeight();
//進行測量
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
當然我們還需要重寫computeScroll()方法(由父視圖調用用來請求子視圖根據偏移值 mScrollX,mScrollY重新繪制 ) 為了實現偏移控制,一般自定義View/ViewGroup都需要重載該方法.
其實移動一個view的簡單三部曲
- 第一、調用Scroller實例去產生一個偏移控制(對應于startScroll()方法)
- 第二、手動調用invalid()方法去重新繪制,剩下的就是在 computeScroll()里根據當前已經逝去的時間,獲取當前應該偏移的坐標(由Scroller實例對應的computeScrollOffset()計算而得),
- 第三、當前應該偏移的坐標,調用scrollBy()方法去緩慢移動至該坐標處
@Override
public void computeScroll() {
Log.i("AutoScrollListView", "computeScroll");
// 如果返回true,表示動畫還沒有結束
// 因為前面startScroll,所以只有在startScroll完成時 才會為false
if (!mScroller.computeScrollOffset()) { //沒有
Log.i("AutoScrollListView", "compute finish");
if (mAnimating) {
Log.i("AutoScrollListView", "compute ignore runnable");
return;
}
Log.i("AutoScrollListView", "compute send runnable");
removeCallbacks(mLoopRunnable); //移除
postDelayed(mLoopRunnable, DALY_TIME); //重新發送
mAnimating = true;
preY = 0;
//檢測當前的位置,防止位置錯亂
checkPosition();
} else { //動畫沒有結束
mAnimating = false; //動畫標志置為false
Log.i("AutoScrollListView", "compute not finish");
int dY = mScroller.getCurrY() - preY; //獲取到當前的y坐標
///**
//* Scrolls the list items within the view by a specified number of pixels.
// *
//* @param y the amount of pixels to scroll by vertically
// * @see #canScrollList(int)
//*/
// public void scrollListBy(int y) {
// trackMotionScroll(-y, -y);
// }
//ListView的item滾動距離y
ListViewCompat.scrollListBy(this, dY); //
preY = mScroller.getCurrY(); //獲取到當前y
invalidate(); //滾動完成之后重新繪制
}
}
這里面有一個檢測位置防止錯亂的方法
/**
* 檢測位置信息
*/
private void checkPosition() {
if (!mAutoScroll) return;
int targetPosition = -1; //初始化目標位置
//第一個可見的view的位置
int firstVisiblePosition = getFirstVisiblePosition();
if (firstVisiblePosition == 0) {
//如果當前的所在的位置是第一個可見的view的位置,也就是第一個item
AutoScroll autoScroll = (AutoScroll) mInnerAdapter;
targetPosition = mInnerAdapter.getCount() - autoScroll.getVisiableCount() * 2;
}
//最后一個item的位置
int lastVisiblePosition = getLastVisiblePosition();
if (lastVisiblePosition == getCount() - 1) {
AutoScroll autoScroll = (AutoScroll) mOutterAdapter;
targetPosition = autoScroll.getVisiableCount();
}
if (targetPosition >= 0 && firstVisiblePosition != targetPosition) {
setSelection(targetPosition);
}
}
到此差不多就能完成了滾動,接下來就是一些優化了,比如長按點擊,點擊事件,設置自動滑動,停止自動滑動,設置滾動延時時間。
點擊事件(然后在相應的位置進行邏輯處理)
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mMoveDistance = 0;
mPreX = ev.getX();
mPreY = ev.getY();
mIgnoreLongClick = false;
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
//移動的距離
mMoveDistance += (Math.abs(ev.getX() - mPreX) + Math.abs(ev.getY() - mPreY));
mPreX = ev.getX();
mPreY = ev.getY();
//移動的距離大于指定值 并且當前的滾動還沒有完成
if (mMoveDistance > 20 || !mScroller.isFinished()) {
mIgnoreLongClick = true;
}
return true;
} else if (ev.getAction() == MotionEvent.ACTION_UP
|| ev.getAction() == MotionEvent.ACTION_CANCEL) {
if (mMoveDistance > 20 || !mScroller.isFinished()) {
//取消長按時間
ev.setAction(MotionEvent.ACTION_CANCEL);
}
mIgnoreLongClick = false;
}
return super.onTouchEvent(ev);
}
class InnerOnItemLongClickListener implements OnItemLongClickListener {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view,
int position, long id) {
return mOutterOnItemLongClickListener != null && mInnerAdapter != null && !mIgnoreLongClick && mOutterOnItemLongClickListener.onItemLongClick(parent, view, (int) mInnerAdapter.getItemId(position), id);
}
}
//長按事件處理
@Override
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
if (mInnerOnItemLongClickListener == null) {
mInnerOnItemLongClickListener = new InnerOnItemLongClickListener();
}
mOutterOnItemLongClickListener = listener;
super.setOnItemLongClickListener(mInnerOnItemLongClickListener);
}
自動和停止滾動
/**
* 開始自動滾動
*/
public void startAutoScroll() {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
removeCallbacks(mLoopRunnable);
mAnimating = false;
post(mLoopRunnable);
}
/**
* 停止自動滾動
*/
public void stopAutoScroll() {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
removeCallbacks(mLoopRunnable);
mAnimating = false;
}
滾動延時時間
/**
* 設置延時事件
*
* @param dalyTime 延時事件 單位: ms
*/
public static void setDalyTime(int dalyTime) {
DALY_TIME = dalyTime;
}
好了,代碼也就差不多這么多了,注釋也是比較容易理解的,因為是對于ListView的自定義,那么用法和ListViwe的使用時大致類似,只要注意兩個方法,手動實現AutoScrollListView.AutoScroll這個接口
//獲取到當前滾動視圖的高度
@Override
public int getListItemHeight(Context context) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, context.getResources().getDisplayMetrics());
}
@Override
public int getVisiableCount() {
return 2; //顯示滾動的item 的個數
}
其他的都是類似ListView的用法了,這個地方代碼就不進行貼附了,這里直接附上github地址,有需要的和想要學習的可以直接到git上獲取,在此說明,小弟才疏學淺,并不能面面俱到,希望有問題互相交流,共同進步.對于效果圖可以見開頭的兩個gif圖片。
重要的事情說三遍
https://github.com/wuyinlei/MarqueeView
https://github.com/wuyinlei/MarqueeView
https://github.com/wuyinlei/MarqueeView