這是一個支持橫向滑動,並處理了滑動衝突的自定義ViewGroup。幾乎涵蓋了自定義viewGroup的所有知識,對於理解View的相關知識有一定的幫助,是一個不錯的實戰Demo。以下爲功能,所做的處理及對應的知識點。
1.支持橫向滑動
爲了使佈局能夠橫向滑動,需要重寫onTouchEvent()方法,在這個方法中判斷是否爲橫向滑動,如果是的話就使用scrollBy()方法讓佈局內容滑動。當用戶快速滑動時,使用Tracker判斷速度是否爲橫向滑動,如果是的話使用Scroller使佈局內容平滑滑動。具體判斷方法如下代碼。(當然需先判斷是否攔截事件)。
//滑動事件處理
@Override
public boolean onTouchEvent(MotionEvent event) {
tracker.addMovement(event);
int x = (int)event.getX();
int y = (int)event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(!scroller.isFinished()){
scroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x-lastX;
int deltaY = y-lastY;
//每次進行滑動限制
scrollBy(-scrollLimit(deltaX),0);
break;
case MotionEvent.ACTION_UP:
int dx=0;
//處理快速滑動
tracker.computeCurrentVelocity(1000);
float xVelocity = tracker.getXVelocity();
if(Math.abs(xVelocity)>=50){
dx = 0-scrollLimit((int)xVelocity);
}
//使用Scroller
smoothScrollBy(dx,0);
tracker.clear();
break;
}
lastX = x;
lastY = y;
return true;
}
關於scrollBy()方面的知識,可參考以下文章:更好地理解 scrollBy() / scrollTo()
2.處理滑動衝突
當佈局的子view爲scrollView或ListView等可以滑動的view時,就需要進行滑動衝突處理,否則最終效果可能與你的目的不同。在這裏只處理子view支持縱向滑動與佈局之間的滑動衝突。簡單來說,就是當爲橫向滑動時,佈局攔截事件,否則傳給子view。
//處理滑動衝突,判斷是否攔截事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Boolean intercept = false;
int x = (int)ev.getX();
int y = (int)ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercept = false;
if(!scroller.isFinished()){
scroller.abortAnimation();
intercept =true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x-lastX;
int deltaY = y-lastY;
//橫向滑動
if(Math.abs(deltaX)>Math.abs(deltaY)){
intercept = true;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
default:
break;
}
//記錄上次事件座標
lastX = x;
lastY = y;
return intercept;
}
3.支持wrap_content屬性
如果不進行處理的話,我們的自定義佈局設置wrap_content屬性跟match_parent屬性效果一樣。具體原因跟view的測量過程有關。可閱讀文章進行了解:Android:爲什麼你的自定義View wrap_content不起作用?
這裏附上處理代碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
measureChildren(widthMeasureSpec,heightMeasureSpec);
int childCount = getChildCount();
maxHeight=0;
maxWidth=0;
for(int i = 0;i<childCount;i++){
View v = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams();
//記錄子view信息,方便佈局
setLocation(v,lp);
}
maxHeight += getPaddingBottom()+getPaddingTop();
maxWidth += getPaddingRight();
//使wrap_content屬性起作用
if(getLayoutParams().width == LayoutParams.WRAP_CONTENT && getLayoutParams().height == LayoutParams.WRAP_CONTENT){
int mWidth = Math.min(maxWidth,widthSize);
setMeasuredDimension(mWidth,maxHeight);
}else if(getLayoutParams().height == LayoutParams.WRAP_CONTENT){
setMeasuredDimension(widthSize,maxHeight);
}else if(getLayoutParams().width == LayoutParams.WRAP_CONTENT){
int mWidth = Math.min(maxWidth,widthSize);
setMeasuredDimension(mWidth,heightSize);
}else {
setMeasuredDimension(widthSize,heightSize);
}
}
4.滑動範圍限制
爲了更好的體驗,我們需對滑動範圍進行限制,不然的話會無限滑動,看到的是一片空白。這裏限制的範圍是佈局子view的總寬度,即滑到子view邊緣就不再滑動了。這裏比較容易搞錯正負值,需要注意。代碼如下:
//限制滑動距離
private int scrollLimit(int delta){
//子view總長度小於佈局寬度,禁止滑動
if(maxWidth-getWidth()<=0){
return 0;
}else {
if (delta <= 0) {
//左滑
if (getScrollX() == maxWidth - getWidth()) {
//處於最右邊,右邊緣可見 ,禁止繼續左滑
return 0;
}else{
//限制滑動距離,使左滑不超過子view內容的右邊緣
int dx = Math.min(maxWidth - getWidth() - getScrollX(), Math.abs(delta));
return 0 - dx;
}
} else {
//右滑
if (getScrollX() == 0) {
//處於開始狀態,左邊緣可見,禁止繼續右滑
return 0;
}else{
//限制滑動距離,使右滑不超過子view內容的左邊緣
return Math.min(Math.abs(getScrollX()),delta);
}
}
}
}
5.處理margin/padding
爲了使佈局支持padding及子view間的margin,在進行佈局時需將此考慮在內。當我們在記錄子view位置信息時就將此考慮在內,代碼如下。(其中ViewLocation爲記錄子view位置信息所創建的類)
//保存各view的位置參數(處理margin)
private void setLocation(View v,MarginLayoutParams lp){
ViewLocation mLocation = new ViewLocation();
mLocation.setLeft(left+lp.leftMargin);
mLocation.setRight(mLocation.getLeft()+v.getMeasuredWidth());
mLocation.setTop(getPaddingTop()+lp.topMargin);
mLocation.setBottom(mLocation.getTop()+v.getMeasuredHeight());
maxWidth += mLocation.getRight()+lp.rightMargin-mLocation.getLeft()+lp.leftMargin;
left += mLocation.getRight()+lp.rightMargin-mLocation.getLeft()+lp.leftMargin;
maxHeight = (mLocation.getBottom()-mLocation.getTop()+lp.bottomMargin+lp.topMargin)>=maxHeight?mLocation.getBottom()-mLocation.getTop()+lp.bottomMargin+lp.topMargin:maxHeight;
viewLocationList.add(mLocation);
}
注意我們獲取子view的margin屬性的方法是先通過獲取它的MarginLayoutParams(上圖方法第2個參數)。
MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams();
此時我們需要重寫 generateLayoutParams()方法,否則會報錯,類型轉換錯誤,因爲這個方法默認返回空值。更多相關的內容可閱讀文章:你的自定義View是否真的支持Margin
//爲獲取子view margin屬性,重寫方法(否則會報錯)
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(),attrs);
}
完整代碼見GitHub:https://github.com/YangRT/HorizontalScrollView