一、先看效果
前段時間在交流羣裏看到有小夥伴在問一個消息滾動列表怎麼做,正好最近在學校準備畢業答辯,公司請了兩週假,忙裏偷閒把這個效果實現了,整體感覺還是不錯的,代碼量也比較少,練練手的同時也給小夥伴們分享一下。先上圖:
二、實現原理
- 就這個效果第一眼看到的時候有點想用自定View來寫,感覺有點像歌詞翻動的效果,不過思考最後還是沒有用這個方案,主要是實現起來有點麻煩,而且這個效果用自定義ViewGroup來做更爲方便,邏輯也更清晰。
整個控件爲一個繼承自FramLayout的ViewGroup,內部放置兩個子控件,用於視圖的切換,爲了響應外部點擊事件,我用了兩個Button作爲子控件,當然也可以用TextView。在初始化時佈局內部兩個子控件的位置,第一個子控件位於第二個子控件的上方,
getChildAt(0).layout(0,0,mWidth,mHeight);
getChildAt(1).layout(0,mHeight,mWidth,mHeight*2);
- 而我們的整個控件大小與子控件大小一致。之後滾動整個控件,這時也就形成了第一個控件往上走而第二個控件在後面跟着的效果,在下方子控件完全顯示在界面中間時我們要做的處理是重新佈局整個頁面,將上部不可見的子控件調到顯示控件的下方,並在這個時候改變不可見控件的內容,用OnPreTextChangeListener來供外部改變子控件的內容,需要注意的是,子控件默認都應該是TextView或者其子類型。以此實現切換效果,這一步我是這麼做的:
//改變設置佈局參數 爲了顯示停頓效果 這裏延遲了3秒滾動
ischange = !ischange;
postDelayed(new Runnable() {
@Override
public void run() {
mScroller.startScroll(0, 0, 0, mHeight, 2000);
scrollTo(0, 0);
requestLayout();
if(getChildAt(0).getBottom() == mHeight){
mlistener.SetOnPreTextChangeListener((TextView) getChildAt(0));
}else if(getChildAt(1).getBottom() == mHeight){
mlistener.SetOnPreTextChangeListener((TextView) getChildAt(1));
}
postInvalidate();
}
},3000);
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// Log.e("TAG","onLayout");
//來回切換內部兩個控件的位置 形成來回滑動效果
if(ischange){
getChildAt(1).layout(0,0,mWidth,mHeight);
getChildAt(0).layout(0,mHeight,mWidth,mHeight*2);
}else{
getChildAt(0).layout(0,0,mWidth,mHeight);
getChildAt(1).layout(0,mHeight,mWidth,mHeight*2);
}
}
最後,在界面從window移除是做點掃尾工作:
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mScroller.abortAnimation();
getHandler().removeCallbacksAndMessages(null);
}
三、具體實現
/** 改變佈局*/
private boolean ischange = false;
/** */
private Scroller mScroller;
/** 高度*/
private int mHeight ;
/** 寬度*/
private int mWidth ;
/** 文字改變前的監聽 在這裏給控件設置內容*/
private OnPreTextChangeListener mlistener;
public void setOnPreTextChangeListener(OnPreTextChangeListener mlistener){
this.mlistener = mlistener;
}
public RollView(Context context) {
this(context, null);
}
public RollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RollView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public RollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mScroller = new Scroller(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//獲取控件寬高
mHeight = getChildAt(0).getMeasuredHeight();
mWidth = getChildAt(0).getMeasuredWidth();
}
@Override
public void computeScroll() {
//判斷scroller是否滾動結束
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}else if (getScrollY() == mHeight){
//改變設置佈局參數
ischange = !ischange;
postDelayed(new Runnable() {
@Override
public void run() {
mScroller.startScroll(0, 0, 0, mHeight, 2000);
scrollTo(0, 0);
requestLayout();
if(getChildAt(0).getBottom() == mHeight){
mlistener.SetOnPreTextChangeListener((TextView) getChildAt(0));
}else if(getChildAt(1).getBottom() == mHeight){
mlistener.SetOnPreTextChangeListener((TextView) getChildAt(1));
}
postInvalidate();
}
},3000);
}else if(getScrollY() == 0){
//第一次進來
postDelayed(new Runnable() {
@Override
public void run() {
mScroller.startScroll(0, 0, 0, mHeight,2000);
postInvalidate();
}
},3000);
}
}
public interface OnPreTextChangeListener{
void SetOnPreTextChangeListener(TextView tv);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if(getChildCount() != 2){
new IllegalArgumentException("must be has 2 children.");
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mScroller.abortAnimation();
getHandler().removeCallbacksAndMessages(null);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// Log.e("TAG","onLayout");
//來回切換內部兩個控件的位置 形成來回滑動效果
if(ischange){
getChildAt(1).layout(0,0,mWidth,mHeight);
getChildAt(0).layout(0,mHeight,mWidth,mHeight*2);
}else{
getChildAt(0).layout(0,0,mWidth,mHeight);
getChildAt(1).layout(0,mHeight,mWidth,mHeight*2);
}
}
mainactivity:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private int num = 0;
private String[] mStrData = {"蘇寧看不下去,全場免費送!","58同城來湊熱鬧!","京東大促,全場5元!","天貓不服,全場不要錢!"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RollView rv = (RollView) findViewById(R.id.rollview);
Button mbtn_jingdong = (Button) findViewById(R.id.jingdong);
Button mbtn_tianmao = (Button) findViewById(R.id.tianmao);
mbtn_jingdong.setOnClickListener(this);
mbtn_tianmao.setOnClickListener(this);
rv.setOnPreTextChangeListener(new RollView.OnPreTextChangeListener() {
@Override
public void SetOnPreTextChangeListener(TextView tv) {
if(num >= mStrData.length){
num = 0;
}
tv.setText(mStrData[num]);
num++;
}
});
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.jingdong:
case R.id.tianmao:
Toast.makeText(this,((TextView)v).getText().toString().trim(),Toast.LENGTH_SHORT).show();
break;
}
}
}
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<path.example.com.mycustom.RollView
android:id="@+id/rollview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
>
<Button
android:id="@+id/jingdong"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="京東大促,全場5元!"
android:textColor="#f60"
android:drawableLeft="@mipmap/tm_rate_icon_star_full"
android:background="#e2e2e2"
android:textSize="20sp"/>
<Button
android:id="@+id/tianmao"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="天貓不服,全場不要錢!"
android:textColor="#f60"
android:drawableLeft="@mipmap/tm_rate_icon_star_full"
android:background="#e2e2e2"
android:textSize="20sp"/>
</path.example.com.mycustom.RollView>
</RelativeLayout>
- 整體實現相對不是很複雜,主要有幾點:動態的給子控件佈局和賦值,在整體實現過程中用到了Scroller這個輔助類,下面就簡單講下這個輔助類的使用。
四、Scroller
Scroller本身不能讓控件滾動,要讓控件滾動要用到View的computeScroll方法,在computeScroll方法中進行判斷是否滾動結束,對控件做相應的處理。Scroller的幾個主要方法:
//開始滾動 startX startY 開始的X,Y值 dx dy 水平和垂直位置滾動的距離 duration 滾動所用的時間 默認爲250毫秒
public void startScroll(int startX, int startY, int dx, int dy, int duration) ;
//是否滾動到指定位置
public boolean computeScrollOffset();
//是否滾動完成
public final boolean isFinished() ;
//中斷動畫 cause the scroller to move to the final x and y position
public void abortAnimation() ;