進階之路:自定義View一仿寶頭條1號店京東垂直滾動的公告(廣告)條
京東首頁公告那一欄的廣告條可以向上滾動,每次展示一條廣告,展示一定時間後,第二條廣告從下往上頂起。依次循環播放效果如圖
—-這個view具有很大的擴展性,你可以修改爲任意垂直滾動的佈局。
1、分析
既然要實現滾動的效果,就需要有一個容器容納當前展示的條目,還有一個容器在下面作爲預備展示的容器,需要展示幾條就動態的向容器中添加指定數量的子條目;最外層是一個大的容器,如果將他的高度設置爲小容器的高度,即可實現遮擋預備容器的目的;滾動可使用動畫集合,讓兩個容器同時向上滾動;滾動結束後,馬上讓被頂上去的容器復位到預備位置;這裏需要兩個引用指向當前展示的容器和預備容器,當動畫結束之後,這兩個引用需要互換。經過一段時間停留後重複上述步驟即可。
思路是有了,要實現起來得考慮細節了。最外層用什麼包裹?繼承ViewGroup?太麻煩(得重寫onLayout),我要實現的效果就是裏面的兩個容器在開始的時候能夠垂直向下排列好即可,所以最簡單的就是LinearLayout,裏面的容器就不用說了,子條目都是垂直向下排列,肯定也是LinearLayout。
2、定義組合控件佈局
這裏第一步就是定義好這些控件組合:既然控件需要上述的滾動效果,按照上面的分析自然得需要至少兩個LinearLayout,下面是auto_vertical_scroll_view_layout.xml佈局:
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:id="@+id/up_lin"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</LinearLayout>
<LinearLayout
android:id="@+id/down_lin"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</LinearLayout>
</LinearLayout >
3、繼承LinearLayout控件
上面的控件組合定義好之後,下面就繼承LinearLayout去包裹他,然後覆蓋其構造方法,使用LayoutInflater將控件組合掛在自己身上,並完成容器內控件的初始化:
public class AutoVerticalScrollView extends LinearLayout implements View.OnClickListener {
protected LinearLayout upLin;
protected LinearLayout downLin;
protected TextView text;
Context mContext;
private int scrollHeight;
private ImageView imageUp;
private TextView textUp;
private ImageView imageDown;
private TextView textDown;
int PeriodTime=2000;//滾動間隔時間及滾動時間
public AutoVerticalScrollView(Context context) {//通過new方式調用
this(context, null, 0);
}
public AutoVerticalScrollView(Context context, AttributeSet attrs) {//通過xml方式調用
this(context, attrs, 0);
}
public AutoVerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
init();
}
public void init() {
this.setGravity(VERTICAL);
LayoutInflater inflater = LayoutInflater.from(mContext);
inflater.inflate(R.layout.auto_vertical_scroll_view_layout, this, true);
upLin = (LinearLayout) this.findViewById(R.id.up_lin);
downLin = (LinearLayout) this.findViewById(R.id.down_lin);
}
}
4、重寫onMeasure
由於每次只能顯示需要展示的容器,遮蓋預備容器,所以只能設置整個高度的一半,這裏使用一個小技巧,由於最外層是LinearLayout,並且是豎直向下的,自帶的LinearLayout的onMeasure()方法完成之後組合控件的高度就是兩個子容器的高度了,所以直接調用super.onMeasuer()之後,再設置高度爲getMeasureHeight()/2即可:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.e("hzm", "getMeasuredHeight1:" + getMeasuredHeight());
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight() / 2);
scrollHeight = getMeasuredHeight();
Log.e("hzm", "getMeasuredHeight2:" + getMeasuredHeight());
}
5 初始數據裝載
首先我們得暴露一個方法出去給外部的調用者,讓其把數據傳進來,爲了簡單我們就使用鏈表,方法如下:
ArrayList<DataBean> mDataList, mTempDataList;
//mTempDataList用於控制剩下未顯示的條數,mDataList原始數據
OnClickCallBack mCallBack;
public void setData(ArrayList<DataBean> mlist,OnClickCallBack mCallBack) {
if (mlist != null && mlist.size() > 0) {
mDataList = mlist;
bindData();
this.mCallBack=mCallBack;
} else {
throw new IllegalArgumentException("數據不能爲空!");
}
}
有了數據就得加載,首次裝載數據的時候得把item中的view也裝載進去,不然現在只有兩個空的LinearLayout如何顯示,因此此處就需要爲LinearLayout加載view進去我命名爲auto_vertical_scroll_view_item.xml(爲了簡單這兒只有圖片和文字,你可以自己修改爲你的佈局)代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/image"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="centerCrop"
/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textSize="16sp"
android:textColor="#000"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"/>
</LinearLayout>
加載完容器的view後就得加載容器中的數據,我們爲了滾動一次就加載兩條鏈表中的數據,加載後就通過handler通知其開始滾動了。
private void bindData() {//首次綁定數據需要綁定view
upLin.removeAllViews();
downLin.removeAllViews();
LayoutInflater inflater = LayoutInflater.from(mContext);
View viewUp = inflater.inflate(R.layout.auto_vertical_scroll_view_item, null, false);//第一條的item
View viewDown = inflater.inflate(R.layout.auto_vertical_scroll_view_item, null, false);//第2條的item
upLin.addView(viewUp);
downLin.addView(viewDown);
imageUp = (ImageView) upLin.findViewById(R.id.image);
textUp = (TextView) upLin.findViewById(R.id.text);
imageDown = (ImageView) downLin.findViewById(R.id.image);
textDown = (TextView) downLin.findViewById(R.id.text);
textUp.setOnClickListener(this);
textDown.setOnClickListener(this);
imageUp.setOnClickListener(this);
imageDown.setOnClickListener(this);
if (mDataList.size() > 1) mTempDataList = (ArrayList<DataBean>) mDataList.clone();
else if (mDataList.size() == 1) {
//如果只有一條數據就copy一條,讓其反覆播放此條數據
mTempDataList=new ArrayList<>();
mTempDataList.add(mDataList.get(0));
mTempDataList.add(mDataList.get(0));
}
if (mTempDataList!=null && mTempDataList.size()>0) {//加載第一條數據
textUp.setText(mTempDataList.get(0).getText());
textUp.setTag(mTempDataList.get(0));
imageUp.setImageResource(mTempDataList.get(0).getImgResId());
imageUp.setTag(mTempDataList.get(0));
mTempDataList.remove(0);
//將當前正在顯示的數據從鏈表刪除
}
if (mTempDataList!=null && mTempDataList.size()>0) {//加載第2條數據
textDown.setText(mTempDataList.get(0).getText());
textDown.setTag(mTempDataList.get(0));
imageDown.setImageResource(mTempDataList.get(0).getImgResId());
imageDown.setTag(mTempDataList.get(0));
}
handler.sendEmptyMessageDelayed(MSG_SCROL, PeriodTime); //開始輪播
}
6、動畫完成後交換容器並重新裝載數據
在view滾動完畢後就需要將第一個LinearLayout移動到第二個LinearLayout的下方,以備下次滾動並從新爲LinearLayout中的view加載數據,並將顯示的哪條數據從鏈表中刪除,當刪除到最後一個時,就爲一次滾動完畢。需要第二次滾動時,我們就把原始數據重新克隆給鏈表,以此實現循環。
private final int MSG_SCROL = 1;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.e("hzm", "handleMessage:");
if (msg.what == MSG_SCROL) {
//繼續動畫
startAnimation();
Log.e("hzm", "startAnimation:");
}
}
};
private void startAnimation() {
//當前展示的容器,從當前位置(0),向上滾動scrollHeight
ObjectAnimator anim1 = ObjectAnimator.ofFloat(upLin, "Y", upLin.getY(), upLin.getY() - scrollHeight);
//預備容器,從當前位置,向上滾動scrollHeight
ObjectAnimator anim2 = ObjectAnimator.ofFloat(downLin, "Y", downLin.getY(), downLin.getY() - scrollHeight);
AnimatorSet animSet = new AnimatorSet();
animSet.setDuration(PeriodTime);
animSet.playTogether(anim1, anim2);
animSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
Log.e("hzm", "upLin動畫開始前位置:" + upLin.getX() + "*" + upLin.getY());
Log.e("hzm", "downLin動畫開始前位置:" + downLin.getX() + "*" + downLin.getY());
}
@Override
public void onAnimationEnd(Animator animation) {
Log.e("hzm", "scrollHeight:" + scrollHeight);
Log.e("hzm", "upLin動畫結束後位置:" + upLin.getX() + "*" + upLin.getY());
Log.e("hzm", "downLin動畫結束後位置:" + downLin.getX() + "*" + downLin.getY());
//滾動結束後,upLin的位置變成了-scrollHeight,這時將他移動到最底下
downLin.setY(0);
//down的位置變爲0,也就是當前看見的
upLin.setY(scrollHeight);
//引用交換
LinearLayout temp = upLin;
upLin = downLin;
downLin = temp;
//給控件綁定新數據
switchData();
//停留指定時間後,重複動畫
handler.sendEmptyMessageDelayed(MSG_SCROL, PeriodTime);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animSet.start();
}
private void switchData() {//動畫完成後重新裝載數據
imageUp = (ImageView) upLin.findViewById(R.id.image);
textUp = (TextView) upLin.findViewById(R.id.text);
imageDown = (ImageView) downLin.findViewById(R.id.image);
textDown = (TextView) downLin.findViewById(R.id.text);
if (mTempDataList!=null && mTempDataList.size()>0) {//加載第一條數據
textUp.setText(mTempDataList.get(0).getText());
textUp.setTag(mTempDataList.get(0));
imageUp.setImageResource(mTempDataList.get(0).getImgResId());
imageUp.setTag(mTempDataList.get(0));
mTempDataList.remove(0); //將當前正在顯示的數據從鏈表刪除
}
if (mTempDataList!=null && mTempDataList.size()>0) {//加載第2條數據
textDown.setText(mTempDataList.get(0).getText());
textDown.setTag(mTempDataList.get(0));
imageDown.setImageResource(mTempDataList.get(0).getImgResId());
imageDown.setTag(mTempDataList.get(0));
}else {//如果沒有數據了就加載原始數據的第一條
textDown.setText(mDataList.get(0).getText());
textDown.setTag(mDataList.get(0));
imageDown.setImageResource(mDataList.get(0).getImgResId());
imageDown.setTag(mDataList.get(0));
}
//重新判斷數據顯示後的剩餘條數 ,如果沒有數據了就就重新把數據克隆給呆顯示的鏈表
if (mTempDataList==null||mTempDataList.size()==0) mTempDataList = (ArrayList<DataBean>) mDataList.clone();
if (mDataList.size() == 1) { //如果只有一條數據就copy一條,讓其反覆播放此條數據
mTempDataList.add(mDataList.get(0));
mTempDataList.add(mDataList.get(0));
}
}
7、條目點擊事件
在組合控件中寫一個條目點擊事件的接口,在動態添加子條目時,爲子條目添加點擊事件,通過view.getTag()(綁定數據時,通過view.setTag()將數據對象設置給子條目view)將當前點擊的子條目對應的數據對象返回即可:
@Override
public void onClick(View v) {
mCallBack.onClickBack((DataBean) v.getTag());
}
public interface OnClickCallBack {
void onClickBack(DataBean dataBean);
}
源碼下載:
http://download.csdn.net/detail/hzmming2008/9876379
第一次寫技術博客居然花了三天時間,而寫這篇代碼只用了三個小時,真的不容易,請多支持,以後這個博客會寫更多的自定義view的文章,幫助新手進階。