科普一下
- 2013年谷歌i/o大會上介紹了兩個新的layout: SlidingPaneLayout和DrawerLayout斜纪,現(xiàn)在這倆個類被廣泛的運(yùn)用铆隘,其實(shí)研究他們的源碼你會發(fā)現(xiàn)這兩個類都運(yùn)用了ViewDragHelper來處理拖動鸠真。ViewDragHelper是Framework中不為人知卻非常有用的一個工具馍刮。ViewDragHelper解決了android中手勢處理過于復(fù)雜的問題徊件,在DrawerLayout出現(xiàn)之前梭依,側(cè)滑菜單都是由第三方開源代碼實(shí)現(xiàn)的卖擅,其中著名的當(dāng)屬M(fèi)enuDrawer鸣奔,MenuDrawer重寫onTouchEvent方法來實(shí)現(xiàn)側(cè)滑效果,代碼量很大惩阶,實(shí)現(xiàn)邏輯也需要很大的耐心才能看懂挎狸。如果每個開發(fā)人員都從這么原始的步奏開始做起,那對于安卓生態(tài)是相當(dāng)不利的断楷。所以說ViewDragHelper等的出現(xiàn)反映了安卓開發(fā)框架已經(jīng)開始向成熟的方向邁進(jìn)锨匆。引用了網(wǎng)上的片段,其實(shí)說了那么多脐嫂,ViewDragHelper就如名字說的一樣统刮,一個拖拽View的幫助類。只要我們這些普通開發(fā)者使用這個類账千,就可以輕松實(shí)現(xiàn)以前寫了一大堆代碼才實(shí)現(xiàn)的效果侥蒙,代碼更簡潔,更優(yōu)雅匀奏。
ViewDragHelper基本用法
- 創(chuàng)建ViewDragHelper鞭衩,建議在ViewGroup內(nèi)部創(chuàng)建使用,而不是在外面使用娃善。
//第二個參數(shù)設(shè)置滑動靈敏度
mViewDragHelper = ViewDragHelper.create(this, 1.0f,mCallback);
- 看看mCallback回調(diào)接口ViewDragHelper.Callback论衍,到底有什么重要的方法,直接上源碼。
public static abstract class Callback {
/**
* 當(dāng)ViewDragHelper狀態(tài)發(fā)生變化時回調(diào)(IDLE,DRAGGING,SETTING[自動滾動時])
* @see #STATE_IDLE
* @see #STATE_DRAGGING
* @see #STATE_SETTLING
*/
public void onViewDragStateChanged(int state) {}
/**
* 當(dāng)captureView的位置發(fā)生改變時回調(diào)
* @param changedView View whose position changed
* @param left New X coordinate of the left edge of the view
* @param top New Y coordinate of the top edge of the view
* @param dx Change in X position from the last call
* @param dy Change in Y position from the last call
*/
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
/**
*當(dāng)CaptureView被捕獲時回調(diào)
* @param capturedChild Child view that was captured
* @param activePointerId Pointer id tracking the child capture
*/
public void onViewCaptured(View capturedChild, int activePointerId) {}
/**
*手指釋放的時候回調(diào)
*
* @param releasedChild The captured child view now being released
* @param xvel X velocity of the pointer as it left the screen in pixels per second.
* @param yvel Y velocity of the pointer as it left the screen in pixels per second.
*/
public void onViewReleased(View releasedChild, float xvel, float yvel) {}
/**
* 當(dāng)觸摸到邊界時回調(diào)聚磺。
*
* @param edgeFlags A combination of edge flags describing the edge(s) currently touched
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void onEdgeTouched(int edgeFlags, int pointerId) {}
/**
*true的時候會鎖住當(dāng)前的邊界坯台,false則unLock。
*
* @param edgeFlags A combination of edge flags describing the edge(s) locked
* @return true to lock the edge, false to leave it unlocked
*/
public boolean onEdgeLock(int edgeFlags) {
return false;
}
/**
*
*在邊界拖動時狀態(tài)回調(diào)
* @param edgeFlags A combination of edge flags describing the edge(s) dragged
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
/**
* 改變同一個坐標(biāo)(x,y)去尋找captureView位置的方法
*
* @param index the ordered position to query for
* @return index of the view that should be ordered at position <code>index</code>
*/
public int getOrderedChildIndex(int index) {
return index;
}
/**
*
*水平方向瘫寝,子View要是消耗事件蜒蕾,就要重寫此方法返回大于1的數(shù)稠炬。
* @param child Child view to check
* @return range of horizontal motion in pixels
*/
public int getViewHorizontalDragRange(View child) {
return 0;
}
/**
*垂直方向,子View要是消耗事件咪啡,就要重寫此方法返回大于1的數(shù)首启。
*
* @param child Child view to check
* @return range of vertical motion in pixels
*/
public int getViewVerticalDragRange(View child) {
return 0;
}
/**
*tryCaptureView如何返回ture則表示可以捕獲該view即是哪個View可以滑動,哪個不可以滑動撤摸,在這個方
*法里面控制
* @param child Child the user is attempting to capture
* @param pointerId ID of the pointer attempting the capture
* @return true if capture should be allowed, false otherwise
*/
public abstract boolean tryCaptureView(View child, int pointerId);
/**
* 水平滑動的邊界處理
*
*
* @param child Child view being dragged
* @param left Attempted motion along the X axis
* @param dx Proposed change in position for left
* @return The new clamped position for left
*/
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
/**
* 垂直滑動的邊界處理
*
*
* @param child Child view being dragged
* @param top Attempted motion along the Y axis
* @param dy Proposed change in position for top
* @return The new clamped position for top
*/
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
}
大概了解所有的回調(diào)方法后毅桃,回歸主題。
一個上拉抽屜布局准夷,我腦子里首先想到的就是這樣一個東東钥飞,圖1。
既然是這樣冕象,新建一個PullUpDragLayout直接繼承ViewGroup代承,代碼如下:
/**
* @作 用:上拉的抽屜布局
* @創(chuàng) 建 人: linguoding 郵箱:linggoudingg@gmail.com
* @日 期: 2016年11月16日 10:32
*/
public class PullUpDragLayout extends ViewGroup {
private ViewDragHelper mViewDragHelper;//拖拽幫助類
private View mBottomView;//底部內(nèi)容View
private View mContentView;//內(nèi)容View
private int mBottomBorderHeigth = 20;//底部邊界凸出的高度
public PullUpDragLayout(Context context) {
super(context);
}
public PullUpDragLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PullUpDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
}
}
PullUpDragLayout 有兩個子View,這兩個子View怎么創(chuàng)建渐扮?可以java代碼new,可以在xml布局里面寫掖棉,也可以自定義屬性墓律,引用屬性得到。new 的話就pass掉幔亥,這里直接使用后面兩種方式耻讽。
<declare-styleable name="PullUpDragLayout">
<attr name="PullUpDrag_BottomView" format="reference"/><!--底部View-->
<attr name="PullUpDrag_ContentView" format="reference"/><!--內(nèi)容View-->
<attr name="PullUpDrag_BottomBorderHeigth" format="reference|integer|dimension"/><!--邊界高度-->
</declare-styleable>
有了屬性,接下來就可以實(shí)例化mBottomView 帕棉, mContentView 针肥,還有得到邊界高度 mBottomBorderHeigth的值。代碼如下:
/**
* @作 用:上拉的抽屜布局
* @創(chuàng) 建 人: linguoding 郵箱:linggoudingg@gmail.com
* @日 期: 2016年11月16日 10:32
*/
public class PullUpDragLayout extends ViewGroup {
private ViewDragHelper mViewDragHelper;//拖拽幫助類
private View mBottomView;//底部內(nèi)容View
private View mContentView;//內(nèi)容View
private LayoutInflater mLayoutInflater;
private int mBottomBorderHeigth = 20;//底部邊界凸出的高度
public PullUpDragLayout(Context context) {
this(context, null, 0);
}
public PullUpDragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PullUpDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
initCustomAttrs(context, attrs);
}
private void init(Context context) {
mLayoutInflater = LayoutInflater.from(context);
}
/**
* 初始化自定義屬性香伴,并實(shí)例化子View
* @param context
* @param attrs
*/
private void initCustomAttrs(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PullUpDragLayout);
if (typedArray != null) {
if (typedArray.hasValue(R.styleable.PullUpDragLayout_PullUpDrag_ContentView)) {
inflateContentView(typedArray.getResourceId(R.styleable.PullUpDragLayout_PullUpDrag_ContentView, 0));
}
if (typedArray.hasValue(R.styleable.PullUpDragLayout_PullUpDrag_BottomView)) {
inflateBottomView(typedArray.getResourceId(R.styleable.PullUpDragLayout_PullUpDrag_BottomView, 0));
}
if (typedArray.hasValue(R.styleable.PullUpDragLayout_PullUpDrag_BottomBorderHeigth)) {
mBottomBorderHeigth = (int) typedArray.getDimension(R.styleable.PullUpDragLayout_PullUpDrag_BottomBorderHeigth, 20);
}
typedArray.recycle();
}
}
private void inflateContentView(int resourceId) {
mContentView = mLayoutInflater.inflate(resourceId, this, true);
}
private void inflateBottomView(int resourceId) {
mBottomView = mLayoutInflater.inflate(resourceId, this, true);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
}
}
實(shí)例化了子View后慰枕,PullUpDragLayout 既然是直接繼承ViewGroup,那肯定要計算子View,還有子View在PullUpDragLayout怎么布局即纲。那就要重寫onMeasure(int widthMeasureSpec, int heightMeasureSpec)具帮,還有onLayout(boolean changed, int left, int top, int right, int bottom)方法。
- onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法計算子View的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mContentView = getChildAt(0);
mBottomView = getChildAt(1);
measureChild(mBottomView, widthMeasureSpec, heightMeasureSpec);
int bottomViewHeight = mBottomView.getMeasuredHeight();
measureChild(mContentView, widthMeasureSpec, heightMeasureSpec);
int contentHeight = mContentView.getMeasuredHeight();
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), bottomViewHeight +contentHeight + getPaddingBottom() + getPaddingTop());
}
- onLayout(boolean changed, int left, int top, int right, int bottom)方法排列子View的位置
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mContentView = getChildAt(0);
mBottomView = getChildAt(1);
mContentView.layout(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), mContentView.getMeasuredHeight());
mBottomView.layout(getPaddingLeft(), mContentView.getHeight() - mBottomBorderHeigth, getWidth() - getPaddingRight(), getMeasuredHeight() - mBottomBorderHeigth);
}
應(yīng)用ViewDragHelper實(shí)現(xiàn)上拉效果
到此低斋,就實(shí)現(xiàn)上面圖1的靜態(tài)效果啦蜂厅。想要底部可以上拉,或者點(diǎn)擊上拉到指定位置膊畴。那就要用到ViewDragHelper掘猿。在init()方法中創(chuàng)建ViewDragHelper.create(this, 1.0f,mCallback)。重寫onInterceptTouchEvent()唇跨、onTouchEvent稠通,將event事件交給ViewDragHelper攔截及處理衬衬,代碼如下:
private void init(Context context) {
mLayoutInflater = LayoutInflater.from(context);
mViewDragHelper = ViewDragHelper.create(this, 1.0f, mCallback);
}
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return mBottomView == child;
}
};
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}
這樣底部抽屜就可以隨意滑動啦,但是這樣還遠(yuǎn)遠(yuǎn)沒有達(dá)到我要的效果采记。我們要實(shí)現(xiàn)以下效果
- 指定方向滑動
- 邊界檢測佣耐、加速度檢測
- 手指抬起,自動展開/收縮
- 點(diǎn)擊Button唧龄,展開/關(guān)閉
- 監(jiān)聽展開/關(guān)閉兼砖,上拉過程發(fā)生改變時回調(diào)
慢慢來,下面一個一個地去實(shí)現(xiàn)既棺。
- 指定方向滑動
控制水平方向不滑動讽挟,上下滑動。重寫ViewDragHelper.Callback中的clampViewPositionHorizontal()方法丸冕,還有這個clampViewPositionVertical()耽梅。代碼如下:
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - mBottomView.getWidth() - leftBound;
final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
int topBound = mContentView.getHeight() - mBottomView.getHeight();
int bottomBound = mContentView.getHeight() - mBottomBorderHeigth;
return Math.min(bottomBound, Math.max(top, topBound));
}
- 手指抬起,自動展開/收縮
重寫onViewReleased()方法:代碼如下:
//手指釋放的時候回調(diào)
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (releasedChild == mBottomView) {
if (releasedChild.getY() < mBoundTopY || yvel <= -1000) {
mViewDragHelper.settleCapturedViewAt(mAutoBackTopPos.x, mAutoBackTopPos.y);
isOpen = true;
if (mOnStateListener != null) mOnStateListener.open();
} else if (releasedChild.getY() >= mBoundTopY || yvel >= 1000) {
mViewDragHelper.settleCapturedViewAt(mAutoBackBottomPos.x, mAutoBackBottomPos.y);
isOpen = false;
if (mOnStateListener != null) mOnStateListener.close();
}
invalidate();
}
}
};
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) { invalidate();
}
}
- 點(diǎn)擊Button胖烛,展開/關(guān)閉
/**
* 切換底部View
*/
public void toggleBottomView() {
if (isOpen) {
mViewDragHelper.smoothSlideViewTo(mBottomView, mAutoBackBottomPos.x, mAutoBackBottomPos.y);
if (mOnStateListener != null) mOnStateListener.close();
} else {
mViewDragHelper.smoothSlideViewTo(mBottomView, mAutoBackTopPos.x, mAutoBackTopPos.y);
if (mOnStateListener != null) mOnStateListener.open();
}
invalidate();
isOpen = !isOpen;
}
- 監(jiān)聽展開/關(guān)閉眼姐,上拉過程發(fā)生改變時回調(diào)
定義兩個接口
public void setOnStateListener(OnStateListener onStateListener) {
mOnStateListener = onStateListener;
}
public void setScrollChageListener(OnScrollChageListener scrollChageListener) {
mScrollChageListener = scrollChageListener;
}
public interface OnStateListener {
void open();
void close();
}
public interface OnScrollChageListener {
void onScrollChange(float rate);
}
重寫onViewPositionChanged 監(jiān)聽位置的改變,計算上拉過程的比率值0-1佩番。代碼如下:
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
if (changedView == mBottomView) {
float startPosition = mContentView.getHeight() - mBottomView.getHeight();
float endPosition = mContentView.getHeight() - mBottomBorderHeigth;
float totalLength = endPosition - startPosition;
float rate = 1 - ((top - startPosition) / totalLength);
if (mScrollChageListener != null) {
mScrollChageListener.onScrollChange(rate);
}
}
}
到此众旗,整個過程就走一遍了。下面放上完整代碼:
/**
* @作 用:上拉的抽屜布局
* @創(chuàng) 建 人: linguoding 郵箱:linggoudingg@gmail.com
* @日 期: 2016年10月28日 14:13
*/
public class PullUpDragLayout extends ViewGroup {
private ViewDragHelper mViewDragHelper;//拖拽幫助類
private View mBottomView;//底部內(nèi)容View
private View mContentView;//內(nèi)容View
LayoutInflater mLayoutInflater;
private int mBottomBorderHeigth = 20;//底部邊界凸出的高度
private Point mAutoBackBottomPos = new Point();
private Point mAutoBackTopPos = new Point();
private int mBoundTopY;
private boolean isOpen;
private OnStateListener mOnStateListener;
private OnScrollChageListener mScrollChageListener;
public void setOnStateListener(OnStateListener onStateListener) {
mOnStateListener = onStateListener;
}
public void setScrollChageListener(OnScrollChageListener scrollChageListener) {
mScrollChageListener = scrollChageListener;
}
public interface OnStateListener {
void open();
void close();
}
public interface OnScrollChageListener {
void onScrollChange(float rate);
}
public PullUpDragLayout(Context context) {
this(context, null, 0);
}
public PullUpDragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PullUpDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
initCustomAttrs(context, attrs);
}
private void init(Context context) {
mLayoutInflater = LayoutInflater.from(context);
mViewDragHelper = ViewDragHelper.create(this, 1.0f,mCallback);
}
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return mBottomView == child;
}
@Override
public int getViewHorizontalDragRange(View child) {
return getMeasuredWidth() - child.getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - mBottomView.getWidth() - leftBound;
final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
int topBound = mContentView.getHeight() - mBottomView.getHeight();
int bottomBound = mContentView.getHeight() - mBottomBorderHeigth;
return Math.min(bottomBound, Math.max(top, topBound));
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
if (changedView == mBottomView) {
float startPosition = mContentView.getHeight() - mBottomView.getHeight();
float endPosition = mContentView.getHeight() - mBottomBorderHeigth;
float totalLength = endPosition - startPosition;
float rate = 1 - ((top - startPosition) / totalLength);
if (mScrollChageListener != null) {
mScrollChageListener.onScrollChange(rate);
}
}
}
//手指釋放的時候回調(diào)
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (releasedChild == mBottomView) {
if (releasedChild.getY() < mBoundTopY || yvel <= -1000) {
mViewDragHelper.settleCapturedViewAt(mAutoBackTopPos.x, mAutoBackTopPos.y);
isOpen = true;
if (mOnStateListener != null) mOnStateListener.open();
} else if (releasedChild.getY() >= mBoundTopY || yvel >= 1000) {
mViewDragHelper.settleCapturedViewAt(mAutoBackBottomPos.x, mAutoBackBottomPos.y);
isOpen = false;
if (mOnStateListener != null) mOnStateListener.close();
}
invalidate();
}
}
};
public boolean isOpen() {
return isOpen;
}
/**
* 切換底部View
*/
public void toggleBottomView() {
if (isOpen) {
mViewDragHelper.smoothSlideViewTo(mBottomView, mAutoBackBottomPos.x, mAutoBackBottomPos.y);
if (mOnStateListener != null) mOnStateListener.close();
} else {
mViewDragHelper.smoothSlideViewTo(mBottomView, mAutoBackTopPos.x, mAutoBackTopPos.y);
if (mOnStateListener != null) mOnStateListener.open();
}
invalidate();
isOpen = !isOpen;
}
private void initCustomAttrs(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PullUpDragLayout);
if (typedArray != null) {
if (typedArray.hasValue(R.styleable.PullUpDragLayout_PullUpDrag_ContentView)) {
inflateContentView(typedArray.getResourceId(R.styleable.PullUpDragLayout_PullUpDrag_ContentView, 0));
}
if (typedArray.hasValue(R.styleable.PullUpDragLayout_PullUpDrag_BottomView)) {
inflateBottomView(typedArray.getResourceId(R.styleable.PullUpDragLayout_PullUpDrag_BottomView, 0));
}
if (typedArray.hasValue(R.styleable.PullUpDragLayout_PullUpDrag_BottomBorderHeigth)) {
mBottomBorderHeigth = (int) typedArray.getDimension(R.styleable.PullUpDragLayout_PullUpDrag_BottomBorderHeigth, 20);
}
typedArray.recycle();
}
}
private void inflateContentView(int resourceId) {
mContentView = mLayoutInflater.inflate(resourceId, this, true);
}
private void inflateBottomView(int resourceId) {
mBottomView = mLayoutInflater.inflate(resourceId, this, true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mContentView = getChildAt(0);
mBottomView = getChildAt(1);
measureChild(mBottomView, widthMeasureSpec, heightMeasureSpec);
int bottomViewHeight = mBottomView.getMeasuredHeight();
measureChild(mContentView, widthMeasureSpec, heightMeasureSpec);
int contentHeight = mContentView.getMeasuredHeight();
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), bottomViewHeight + contentHeight + getPaddingBottom() + getPaddingTop());
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mContentView = getChildAt(0);
mBottomView = getChildAt(1);
mContentView.layout(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), mContentView.getMeasuredHeight());
mBottomView.layout(getPaddingLeft(), mContentView.getHeight() - mBottomBorderHeigth, getWidth() - getPaddingRight(), getMeasuredHeight() - mBottomBorderHeigth);
mAutoBackBottomPos.x = mBottomView.getLeft();
mAutoBackBottomPos.y = mBottomView.getTop();
mAutoBackTopPos.x = mBottomView.getLeft();
mAutoBackTopPos.y = mContentView.getHeight() - mBottomView.getHeight();
mBoundTopY = mContentView.getHeight() - mBottomView.getHeight() / 2;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
invalidate();
}
}
}
XML中使用:
<com.xxx.widget.PullUpDragLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/pull_up_drag_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:PullUpDrag_BottomBorderHeigth="30dp"
app:PullUpDrag_ContentView="@layout/pp_content_view"
app:PullUpDrag_BottomView="@layout/view_bottom"
>
<!--或者在這里趟畏,注意順序不能變 ContentView 再到 BottomView-->
</com.xxx.widget.PullUpDragLayout>
效果大概可以是這樣的:
展開時:
大概就是這樣的...............................