SlidingPaneLayout
, a view that can be dragged from bottom to top and vice versa and the DrawerLayout
, now used in almost all Google applications. Both of these use a new concept to more easily manage dragging: the ViewDragHelper.In this article, I’m going to talk about the ViewDragHelper (aka VDH) because making a custom layout with dragging child view may be pain sometimes. First, I will show you how to use it and how it works (the main lines). Secondly, I will expose you a use case where the VDH is really useful.
Download Sample Application
API design
In a few words
There are some important points to remember about VDH:- a
ViewDragHelper.Callback
is used as a communication channel between parent view and VDH - there is a static factory method to create a VDH instance
- you can configure the drag direction as you want
- a drag can be detected from edge even if there is no view to capture (left, right, top, bottom)
Reading the source code
The VDH and its callback are available in the support-v4 library. You can read the source code : ViewDragHelper and ViewDragHelper.Callback.It uses some common classes of the framework : a VelocityTracker for tracking fingling and other touch events and a Scroller to scroll views when it’s needed.
You must read the source code as much as possible because first, it’s very interesting and then if you know how it works, you will be able to use it in a better way.
Using the VDH
In this section, I’m going to show you a few examples of what is possible to configure on a VDH. Let’s begin with some initializations and then, I will explain a few possible configurations.VDH’s initialization
A customViewGroup
extending a LinearLayout
(DragLayout
) with a simple child View
(named mDragView
).Create a VDH with its callback. Note that you can specify the sensivity (official documentation says Multiplier for how sensitive the helper should be about detecting the start of a drag. Larger values are more sensitive. 1.0f is normal.)public class DragLayout extends LinearLayout { private final ViewDragHelper mDragHelper; private View mDragView; public DragLayout(Context context) { this(context, null); } public DragLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DragLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); }
1 2 3 4 5 |
|
onInterceptTouch
and onTouch
.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Horizontal only
ImplementsclampViewPositionHorizontal
to allow horizontal drag and to bound the drag motion. Note that documentation says The default implementation does not allow horizontal motion.You have to take margins and parent padding into consideration.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Vertical only
ImplementsclampViewPositionVertical
to allow horizontal drag and to bound the drag motion. Note that documentation says The default implementation does not allow vertical motion.You have to take margins and parent padding into consideration. Not like in the code below
1 2 3 4 5 6 7 8 9 |
|
Capture or not capture a view
ImplementstryCaptureView
to allow a child view to be captured. Here, there are two child views (mDragView1
and mDragView2
) but only one (mDragView1
) is draggable.1 2 3 4 |
|
DragRange
ImplementsgetViewHorizontalDragRange
or getViewVerticalDragRange
to returns the range of horizontal|vertical drag in pixels. This range is used by the VDH when you call smoothSlideViewTo
or settleCapturedViewAt
to calculate the scroll duration. Also, it’s used to check the horizontal|vertical touch slop.Edge dragging
This feature is used in theDrawerLayout
with EDGE_LEFT
and EDGE_RIGHT
.Configure the VDH to enable edge tracking.
|
onEdgeTouched
called when the configured edge is touched. Note that at this time, no child view is currently captured.
|
onEdgeDragStarted
called when a real drag from the configured edge has started. Note that at this time, no child view is currently captured. In this method, you have to capture a child view manually.
|
A real example, the Youtube while playing layout
Recently, I’ve received an update of the Youtube app on my phone. Before this update, the most annoying thing was to not be able to watch a video and search the next video at the same time. They fixed this by implementing a nice layout in which you can minimize the video view from top to bottom.I’m going to show how to do it and how it’s simple thanks to VDH.
Here is the expected result
Key points:
tryCaptureView
returns true only for the header view- drag range is calculated
onLayout
- use VDH’s methods in
onInterceptTouchEvent
andonTouchEvent
- call
continueSettling
incomputeScroll
(because VDH uses a scroller) - use
smoothSlideViewTo
to finish the drag motion
onLayout
and onMeasure
are badly written too. Also, I don’t know if calling requestLayout
in onViewPositionChanged
is good solution… Anyway, if you have remarks or ideas to improve this layout, please tell me!).
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="list"
/>
<com.example.vdh.YoutubeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/youtubeLayout"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:id="@+id/viewHeader"
android:layout_width="match_parent"
android:layout_height="128dp"
android:fontFamily="sans-serif-thin"
android:textSize="25sp"
android:tag="text"
android:gravity="center"
android:textColor="@android:color/white"
android:background="#AD78CC"/>
<TextView
android:id="@+id/viewDesc"
android:tag="desc"
android:textSize="35sp"
android:gravity="center"
android:text="Loreum Loreum"
android:textColor="@android:color/white"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF00FF"/>
</com.example.vdh.YoutubeLayout>
</FrameLayout>
public class YoutubeLayout extends ViewGroup{
private static final String TAG = "YoutubeLayout";
private static final int MIN_FLING_VELOCITY = 400; // dips per second
private View mHeaderView;
private View mDescView;
private float mInitialMotionX;
private float mInitialMotionY;
private int mDragRange;
private int mTop;
private float mDragOffset;
private ViewDragHelper mDragHelper;
public YoutubeLayout(Context context) {
super(context);
init(context);
}
public YoutubeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public YoutubeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
final float density = context.getResources().getDisplayMetrics().density;
mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density);
}
public void maximize()
{
smoothSlideTo(0.0f);
}
public void minimize()
{
smoothSlideTo(1.0f);
}
private boolean smoothSlideTo(float slideOffset) {
final int topBound = getPaddingTop();
int y = (int) (topBound + slideOffset * mDragRange);
if(mDragHelper.smoothSlideViewTo(mHeaderView, mHeaderView.getLeft(), y))
{
ViewCompat.postInvalidateOnAnimation(this);
return true;
}
return false;
}
@Override
protected void onFinishInflate() {
Log.d(TAG, "onFInishInflate");
mHeaderView = findViewById(R.id.viewHeader);
mDescView = findViewById(R.id.viewDesc);
}
@SuppressLint("NewApi")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Log.d(TAG, "onMeasure");
measureChildren(widthMeasureSpec, heightMeasureSpec);
int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0), resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int parentViewHeight = getHeight();
int dragViewHeight = mHeaderView.getMeasuredHeight();
mDragRange = parentViewHeight - dragViewHeight;
mHeaderView.layout(
0,
mTop,
r,
mTop + mHeaderView.getMeasuredHeight());
mDescView.layout(
0,
mTop + mHeaderView.getMeasuredHeight(),
r,
mTop + b);
}
private class DragHelperCallback extends ViewDragHelper.Callback
{
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == mHeaderView;
}
@SuppressLint("NewApi")
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
mTop = top;
mDragOffset = (float) top / mDragRange;
mHeaderView.setPivotX(mHeaderView.getWidth());
mHeaderView.setPivotY(mHeaderView.getHeight());
mHeaderView.setScaleX(1 - mDragOffset / 2);
mHeaderView.setScaleY(1 - mDragOffset / 2);
mDescView.setAlpha(1 - mDragOffset);
requestLayout();
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - mHeaderView.getHeight() - mHeaderView.getPaddingBottom();
final int newTop = Math.min(Math.max(top, topBound), bottomBound);
return newTop;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
int top = getPaddingTop();
if (yvel > 0 || (yvel == 0 && mDragOffset > 0.4f)) {
top += mDragRange;
}
mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
invalidate();
}
@Override
public int getViewVerticalDragRange(View child) {
return mDragRange;
}
}
@Override
public void computeScroll() {
if(mDragHelper.continueSettling(true))
{
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if(action != MotionEvent.ACTION_DOWN)
{
mDragHelper.cancel();
return super.onInterceptTouchEvent(ev);
}
if(action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP)
{
mDragHelper.cancel();
return false;
}
final float x = ev.getX();
final float y = ev.getY();
boolean interceptTap = false;
switch(action)
{
case MotionEvent.ACTION_DOWN:
mInitialMotionX = x;
mInitialMotionY = y;
interceptTap = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
break;
case MotionEvent.ACTION_MOVE:
final float adx = Math.abs(x - mInitialMotionX);
final float ady = Math.abs(y - mInitialMotionY);
final int slop = mDragHelper.getTouchSlop();
if(ady > slop && adx > ady)
{
mDragHelper.cancel();
return false;
}
break;
}
return mDragHelper.shouldInterceptTouchEvent(ev) || interceptTap;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragHelper.processTouchEvent(event);
final int action = event.getAction();
final float x = event.getX();
final float y = event.getY();
boolean isHeaderViewUnder = mDragHelper.isViewUnder(mHeaderView, (int)x, (int)y);
switch(action & MotionEventCompat.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
{
mInitialMotionX = x;
mInitialMotionY = y;
break;
}
case MotionEvent.ACTION_UP:
{
final float dx = x - mInitialMotionX;
final float dy = y - mInitialMotionY;
final float slop = mDragHelper.getTouchSlop();
if(dx * dx + dy * dy < slop * slop && isHeaderViewUnder)
{
if (mDragOffset == 0) {
minimize();
} else {
maximize();
}
}
break;
}
}
return isHeaderViewUnder && isViewHit(mHeaderView, (int) x, (int) y) || isViewHit(mDescView, (int) x, (int) y);
}
private boolean isViewHit(View view, int x, int y) {
int[] viewLocation = new int[2];
view.getLocationOnScreen(viewLocation);
int[] parentLocation = new int[2];
this.getLocationOnScreen(parentLocation);
int screenX = parentLocation[0] + x;
int screenY = parentLocation[1] + y;
return screenX >= viewLocation[0] && screenX < viewLocation[0] + view.getWidth() &&
screenY >= viewLocation[1] && screenY < viewLocation[1] + view.getHeight();
}
}
Conclusion
The VDH is one of the useful but unknown class in the framework. Don’t hesitate to try it, use it and to appreciate it because it saves a lot of time and a lot of code!Source: http://flavienlaurent.com/blog/2013/08/28/each-navigation-drawer-hides-a-viewdraghelper/
0 comments:
Post a Comment