Android copy YouTube drag video effect implementation

youtube-like-drag-video-view

The code has been open-source to GitHub

Github.com/Lyzon/youtu…

You can give me a star to support me! Thank you very much!

A rendering of the implementation





demo.gif

Implementation approach

When I saw this effect in the YouTube APP, I thought it was interesting, and I wanted to implement this effect. After thinking for a long time, I came up with the following implementation scheme:

  • I chose TextureView as the first View to play the video. For TextureView, please refer to this article: TextureView Easy Tutorial
  • Customizing our YouTubeVideoView inherits a LinearLayout that wraps TextureView with the details page below.
  • Calculate and change the width and height of TextureView based on how far your finger slides across the screen.
  • I chose to dynamically set the marginTop property via LayoutParams to swipe up and down.
  • While minimizing, judge the user intent first, if is the lateral sliding, change marginRight/Left property to realize the lateral sliding, sliding is a certain distance to hide the whole View.
  • The rest of the sliding effect after the finger is lifted is achieved using property animation.
  • The rest of the details, such as changes in transparency, the hover effect when minimized, and the distance from the screen boundary, are also based on how far the finger slides.
  • All the handling of the slide event is done in the OnTouchListener set for TextureView.

Other implementation ideas

  • TextureView dragging can also be done using an Android dragHelper class called ViewDragHelper, which interconnects with other views in its callbacks.
  • You can try CoordinatorLayout, coordination and linkage. ~

Let’s Code!

I’m not going to post all the code here, but I’m going to focus on a few points that I need to pay attention to in this implementation idea. Let’s take a look at the global variables we’ll be using:

// Draggable videoView and details View below
private View mVideoView;
private View mDetailView;
// The wrapper class of the video class, used for property animation
private VideoViewWrapper mVideoWrapper;

// Slide range. The value is the height from the top of the screen when videoView is minimized
private float allScrollY;

//1f is the initial state, 0.5F or 0.25f(landscape) is the minimum state
private float nowStateScale;
// Minimum scaling
private float MIN_RATIO = 0.5f;
private static final float VIDEO_RATIO = 16f / 9f;

// Whether it is the first Measure, used to obtain the initial width and height of the player
private boolean isFirstMeasure = true;

//VideoView initial width and height
private int originalWidth;
private int originalHeight;

// The minimum distance from the right and bottom of the screen DP values will be initialized to PX
private static final int MARGIN_DP = 12;
private int marginPx;

// Can be swiped to delete
private boolean canHide;Copy the code

Next, you override the onFinishInflate() method to get two child views, one that plays the video and one that shows the details.

@Override
protected void onFinishInflate(a) {
    super.onFinishInflate();
    if(getChildCount() ! =2)
        throw new RuntimeException("YouTubeVideoView only need 2 child views");

    mVideoView = getChildAt(0);
    mDetailView = getChildAt(1);

    init();
}Copy the code

Looking at the init() method, we do the initialization:

private void init() {
    // Set the touch listener
    mVideoView.setOnTouchListener(new VideoTouchListener());
    // Initialize the wrapper class
    mVideoWrapper = new VideoViewWrapper(a);//DP To PX
    marginPx = MARGIN_DP * (getContext().getResources().getDisplayMetrics().densityDpi / 160);

    // Current scale
    nowStateScale = 1f;

    // If it is landscape, the minimum ratio is 0.25F
    if (mVideoView.getContext().getResources().getConfiguration().orientation= =Configuration.ORIENTATION_LANDSCAPE)
        MIN_RATIO = 0.25f;

    originalWidth = mVideoView.getContext().getResources().getDisplayMetrics().widthPixels;
    originalHeight = (int) (originalWidth / VIDEO_RATIO);

    ViewGroup.LayoutParams lp = mVideoView.getLayoutParams(a);lp.width = originalWidth;
    lp.height = originalHeight;
    mVideoView.setLayoutParams(lp);
}Copy the code
  • First we register a listener for the View that plays the video, and then handle the touch events in this listener.
  • We then initialize a wrapper class for the property animation, which we’ll examine later.
  • Px = dp * (dpi / 160).
  • Original width = screen width, height calculated by scaling 16:9.
  • Then set the initial width and height to the View playing the video via LayoutParams.
  • Packing:

      private class VideoViewWrapper {
      private LinearLayout.LayoutParams params;
      private LinearLayout.LayoutParams detailParams;
    
      VideoViewWrapper() {
          params = (LinearLayout.LayoutParams) mVideoView.getLayoutParams();
          detailParams = (LinearLayout.LayoutParams) mDetailView.getLayoutParams();
          params.gravity = Gravity.END;
      }
    
      int getWidth() {
          return params.width < 0 ? originalWidth : params.width;
      }
    
      int getHeight() {
          return params.height < 0 ? originalHeight : params.height;
      }
    
      void setWidth(float width) {
          if (width == originalWidth) {
              params.width = - 1;
              params.setMargins(0.0.0.0);
          } else
              params.width = (int) width;
    
          mVideoView.setLayoutParams(params);
      }
    
      void setHeight(float height) {
          params.height = (int) height;
          mVideoView.setLayoutParams(params);
      }Copy the code

    To change the width and height of a View, you can use onMeasure or onLayout to change the width and height of a View. You can also change the width and height of a View by setting LayoutParams to the View. Then when we use property animation, to change the value of a property of an object, the property must have the corresponding set/get method. However, there is no setWidth/getWidth method in View. Some implementation classes have setWidth/getWidth method. But it is not the width and height of the controls that have changed. In this case, using the wrapper class is a perfect solution to this problem. We indirectly provide get/set width and height methods for the View that plays the video. The implementation inside the method is to set LayoutParams for the View. This is not only readable, but also safe and extensible.

Then rewrite the onMeasure() method. If it’s the first onMeasure, initialize the vertical slide interval, that is, the vertical distance your finger needs to slide during the video View from maximum to minimum, which is the MarginTop value of the video View when minimized. Get the measured height of our entire View by this.getMeasuredHeight(). The reason we don’t use the screen height is because of virtual keystrokes.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (isFirstMeasure) {
        // Slide range, which is the height from the top of the screen when videoView is minimized, marginTop when minimized
        allScrollY = this.getMeasuredHeight() - MIN_RATIO * originalHeight - marginPx;
        isFirstMeasure = false; }}Copy the code

And then I’m going to go to the OnTouchListener that I set for the video View, and there’s a little bit more code in this class, so in ACTION_DOWN, do some initialization, in ACTION_UP and CANCEL, determine the state of the View, maximize or minimize it. Here’s a look at the code in ACTION_MOVE:

case MotionEvent.ACTION_MOVE:
    tracker.addMovement(ev);
    dy = y - mLastY; // The difference between the slide and the previous slide
    int dx = x - mLastX;
    int newMarY = mVideoWrapper.getMargin() + dy; // New marginTop value
    int newMarX = mVideoWrapper.getMarginRight() - dx;// New marginRight value
    int dDownY = y - mDownY;
    int dDownX = x - mDownX; // The difference generated from the click point

    // If the slide reaches a certain distance
    if (Math.abs(dDownX) > touchSlop || Math.abs(dDownY) > touchSlop) {
        isClick = false;
        if (Math.abs(dDownX) > Math.abs(dDownY) && canHide) {// if X>Y and can slide off
            mVideoWrapper.setMarginRight(newMarX);
            } else
              updateVideoView(newMarY); Otherwise update the size with the value of the new marginTop
            }
      break;Copy the code
  • TouchSlop is an int value, which varies with different resolutions. Generally, only when the sliding difference is greater than this value, can the user be considered to be sliding. Get (getContext()).getScaledTouchSlop().
  • Dynamically set the marginRight attribute while swiping to hide.
  • So when we swipe vertically to change the size, we call the updateVideoView(int marginTop) method:

      private void updateVideoView(int m) {
      // If the current state is minimized, set the width and height of our layout to MATCH_PARENT
      if (nowStateScale == MIN_RATIO) {
          ViewGroup.LayoutParams params = getLayoutParams();
          params.width = - 1;
          params.height = - 1;
          setLayoutParams(params);
      }
    
      canHide = false;
    
      //marginTop has a maximum value of allScrollY and a minimum of 0
      if (m > allScrollY)
          m = (int) allScrollY;
      if (m < 0)
          m = 0;
    
      // The percentage of the height of the video View is 100%-0%
      float marginPercent = (allScrollY - m) / allScrollY;
      // The percentage of the corresponding size of the video View is 100%-50% or 25%
      float videoPercent = MIN_RATIO + (1f - MIN_RATIO) * marginPercent;
    
      // Set width and height
      mVideoWrapper.setWidth(originalWidth * videoPercent);
      mVideoWrapper.setHeight(originalHeight * videoPercent);
    
      mDetailView.setAlpha(marginPercent);// Set the transparency of the details View below
      this.getBackground().setAlpha((int) (marginPercent * 255));
    
      int mr = (int) ((1f - marginPercent) * marginPx); // Margin to the right of VideoView and above details View
      mVideoWrapper.setZ(mr / 2);// This is the z-axis value, levitation effect
    
      mVideoWrapper.setMarginTop(m);
      mVideoWrapper.setMarginRight(mr);
      mVideoWrapper.setDetailMargin(mr);Copy the code

    }

The main thing is to calculate the percentage from the marginTop value, and then use the percentage to get the current width and height, which is set to the video View via the wrapper class.

Take a look at the handling in UP:

                case MotionEvent.ACTION_UP:

                if (isClick) {
                    if (nowStateScale == 1f && mCallback ! =null) {
                            // Click event callback
                            mCallback.onVideoClick();
                    } else {
                        goMax();
                    }
                    break;
                }

                tracker.computeCurrentVelocity(100);
                float yVelocity = Math.abs(tracker.getYVelocity());
                tracker.clear(a); tracker.recycle();if (canHide) {
                    // If the speed is greater than a certain value or the sliding distance exceeds the minimum width, hide, otherwise keep the minimum state.
                    if (yVelocity > touchSlop || Math.abs(mVideoWrapper.getMarginRight()) > MIN_RATIO * originalWidth)
                        dismissView();
                    else
                        goMin();
                } else
                    confirmState(yVelocity, dy);// Determine the status.
                break;Copy the code

First, isClick in UP is true if the MOVE in MOVE is less than touchSlop, and then the click event is processed. Break, if not the click event, determines the state based on the speed or distance of the MOVE.

private void confirmState(float v, int dy) { //dy is used to determine whether the slide is in the opposite direction

    // If the finger reaches a certain width or speed, change the state
    if (nowStateScale == 1f) {
        if (mVideoView.getWidth() <= originalWidth * 0.75f || (v > 15 && dy > 0)) {
            goMin();
        } else
            goMax();
    } else {
        if (mVideoView.getWidth() >= originalWidth * 0.75f || (v > 15 && dy < 0)) {
            goMax();
        } else
            goMin(); }}Copy the code

Very simple. Finally, the goMax() function:

public void goMax(a) {

    AnimatorSet set = new AnimatorSet();
    set.playTogether(
            ObjectAnimator.ofFloat(mVideoWrapper, "width", mVideoWrapper.getWidth(), originalWidth),
            ObjectAnimator.ofFloat(mVideoWrapper, "height", mVideoWrapper.getHeight(), originalHeight),
            ObjectAnimator.ofInt(mVideoWrapper, "marginTop", mVideoWrapper.getMarginTop(), 0),
            ObjectAnimator.ofInt(mVideoWrapper, "marginRight", mVideoWrapper.getMarginRight(), 0),
            ObjectAnimator.ofInt(mVideoWrapper, "detailMargin", mVideoWrapper.getDetailMargin(), 0),
            ObjectAnimator.ofFloat(mVideoWrapper, "z", mVideoWrapper.getZ(), 0),
            ObjectAnimator.ofFloat(mDetailView, "alpha", mDetailView.getAlpha(), 1f),
            ObjectAnimator.ofInt(this.getBackground(), "alpha".this.getBackground().getAlpha(), 255));set.setDuration(200).start();
    nowStateScale = 1.0 f;
    canHide = false;
}Copy the code

Use property animation to set all values of all objects to their maximized state. The goMin() method is similar, just set the properties backwards.

Finally, do some regular work in MainActivity and just play the video!

To summarize

This custom ViewGroup of the code there are many can be optimized, but MY level is limited, do not good enough. In addition, this effect cannot be wrapped into a library, because there are many limitations. Write this effect, from the beginning of the complete no thought, to later step by step slowly come out. In fact, it is a very fulfilling thing. This is my first time to write a blog, if there is any bad place please criticize and correct. Thank you very much, the code has been open source to Github, hope you can give a star, thank you again.