開源:YinyuetaiPlayer - 高仿音悅臺播放頁面效果

HannahMoll 7年前發布 | 7K 次閱讀 Android開發 移動開發 RecyclerView

新版的音悅臺 APP 播放頁面交互非常有意思,可以把播放器往下拖動,這個頁面透明漸變,然后到底部可以左右拖動關閉播放器,然后點擊視頻列表有個頁面彈出來的效果,十分炫酷,于是我自己動手實現了這個交互炫酷的播放器頁面。

1.廢話不多說,直接演示實現效果

1.1.點擊某個視頻,然后手指上下拖動,播放器做尺寸比例的漸變,視頻相關信息做透明度漸變

這里寫圖片描述

1.2.播放器只有在底部的時候,才能左右拖動,此時播放器做透明度漸變,拖動一定范圍可以關閉播放器;然后它只有在原始位置的一小段距離內可以往上拖動

這里寫圖片描述

1.3.點擊視頻列表的時候,若是上次視頻是左右拖動關閉的話,會有個彈起播放頁面的效果;若是返回鍵和返回箭頭則無效果

這里寫圖片描述

2.實現的思路講解

  • 毫無疑問,需要自定義一個容器,然后處理它的觸摸事件,對它的子 View 進行不同的處理。觸摸事件的處理使用 ViewDragHelper 是再適合不過了,然后你需要實現容器 onMeasure 和 onLayout,由于使用了 ViewDragHelper,有些坑在代碼解析的時候就會講解。
  • 播放頁面是用新的 Activity 還僅僅是當前 Activity 的View的問題,由于播放器縮小到底部的時候,用戶是可以滑動視頻列表的,所以我個人認為就是在當前 Activity 放置一個自定義容器即可,因此為了效率考慮你可以用 ViewStub 來懶加載處理,這里方便演示我就直接 View 的形式了。

3.代碼解析

3.1.需要的變量

/**
 * Created by Oubowu on 201612/26 13:58.<p>
 * 實現了布局交互的容器
 */
public class YytLayout extends ViewGroup {

    private static final int MIN_FLING_VELOCITY = 400;
    private ViewDragHelper mDragHelper;

    // 拖動的寬度
    private int mDragWidth;
    // 拖動的高度
    private int mDragHeight;

    // 響應拖動做縮放的View
    private View mFlexView;
    // 與mFlexView聯動做透明度漸變的View
    private View mFollowView;

    // 響應拖動做縮放的View保存的位置
    private ChildLayoutPosition mFlexLayoutPosition;
    // 與mFlexView聯動的View保存的位置
    private ChildLayoutPosition mFollowLayoutPosition;

    // 水平拖動與否的標志位
    private boolean mHorizontalDragEnable;

    public boolean isHorizontalDragEnable() {
        return mHorizontalDragEnable;
    }

    // 垂直拖動與否的標志位
    private boolean mVerticalDragEnable = true;

    // 是否正在關閉頁面的標志位
    private boolean mIsClosing;

    // 監聽布局是否水平拖動關閉了
    private OnLayoutStateListener mOnLayoutStateListener;

    // 做拖放縮放的子View的寬度
    private int mFlexWidth;
    // 做拖放縮放的子View的高度
    private int mFlexHeight;

    // mFlexView縮放的比率
    private float mFlexScaleRatio = 1;

    // mFlexView縮放的基準點的偏移值
    private int mFlexScaleOffset;

    // 觸摸事件是否發生在mFlexView的區域
    private boolean mInFlexViewTouchRange;

3.2.初始化做 ViewDragHelper 的初始化,然后 post 拿到兩個子 View,這里強制規定只能有兩個子元素

public YytLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {

        final float density = getResources().getDisplayMetrics().density;
        final float minVel = MIN_FLING_VELOCITY * density;

        ViewGroupCompat.setMotionEventSplittingEnabled(this, false);
        FlexCallback flexCallback = new FlexCallback();
        mDragHelper = ViewDragHelper.create(this, 1.0f, flexCallback);
        // 最小拖動速度
        mDragHelper.setMinVelocity(minVel);

        post(new Runnable() {
            @Override
            public void run() {

                // 需要添加的兩個子View,其中mFlexView作為拖動的響應View,mLinkView作為跟隨View
                mFlexView = getChildAt(0);
                mFollowView = getChildAt(1);

                mDragHeight = getMeasuredHeight() - mFlexView.getMeasuredHeight();

                mFlexWidth = mFlexView.getMeasuredWidth();
                mFlexHeight = mFlexView.getMeasuredHeight();

            }
        });

    }

3.3. ViewDragHelper 的回調需要做的事情比較多,在 mFlexView 拖動的時候需要同時設置 mFlexView 和 mFollowView 的相應變化效果,在 mFlexView 釋放的時候需要處理關閉或收起等效果

private class FlexCallback extends ViewDragHelper.Callback {

        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            // mFlexView來響應觸摸事件
            return mFlexView == child;
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return Math.max(Math.min(mDragWidth, left), -mDragWidth);
        }

        @Override
        public int getViewHorizontalDragRange(View child) {
            return mDragWidth * 2;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            if (!mVerticalDragEnable) {
                // 不允許垂直拖動的時候是mFlexView在底部水平拖動一定距離時設置的,返回mDragHeight就不能再垂直做拖動了
                return mDragHeight;
            }
            return Math.max(Math.min(mDragHeight, top), 0);
        }

        @Override
        public int getViewVerticalDragRange(View child) {
            return mDragHeight;
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {

            if (mHorizontalDragEnable) {
                // 如果水平拖動有效,首先根據拖動的速度決定關閉頁面,方向根據速度正負決定
                if (xvel > 1500) {
                    mDragHelper.settleCapturedViewAt(mDragWidth, mDragHeight);
                    mIsClosing = true;
                } else if (xvel < -1500) {
                    mDragHelper.settleCapturedViewAt(-mDragWidth, mDragHeight);
                    mIsClosing = true;
                } else {
                    // 速度沒到關閉頁面的要求,根據透明度來決定關閉頁面,方向根據releasedChild.getLeft()正負決定
                    float alpha = releasedChild.getAlpha();
                    if (releasedChild.getLeft() < 0 && alpha <= 0.4f) {
                        mDragHelper.settleCapturedViewAt(-mDragWidth, mDragHeight);
                        mIsClosing = true;
                    } else if (releasedChild.getLeft() > 0 && alpha <= 0.4f) {
                        mDragHelper.settleCapturedViewAt(mDragWidth, mDragHeight);
                        mIsClosing = true;
                    } else {
                        mDragHelper.settleCapturedViewAt(0, mDragHeight);
                    }
                }
            } else {
                // 根據垂直方向的速度正負決定布局的展示方式
                if (yvel > 1500) {
                    mDragHelper.settleCapturedViewAt(0, mDragHeight);
                } else if (yvel < -1500) {
                    mDragHelper.settleCapturedViewAt(0, 0);
                } else {
                    // 根據releasedChild.getTop()決定布局的展示方式
                    if (releasedChild.getTop() <= mDragHeight / 2) {
                        mDragHelper.settleCapturedViewAt(0, 0);
                    } else {
                        mDragHelper.settleCapturedViewAt(0, mDragHeight);
                    }
                }
            }
            invalidate();
        }

        @Override
        public void onViewPositionChanged(final View changedView, int left, int top, int dx, int dy) {

            float fraction = top * 1.0f / mDragHeight;

            // mFlexView縮放的比率
            mFlexScaleRatio = 1 - 0.5f * fraction;
            mFlexScaleOffset = changedView.getWidth() / 20;
            // 設置縮放基點
            changedView.setPivotX(changedView.getWidth() - mFlexScaleOffset);
            changedView.setPivotY(changedView.getHeight() - mFlexScaleOffset);
            // 設置比例
            changedView.setScaleX(mFlexScaleRatio);
            changedView.setScaleY(mFlexScaleRatio);

            // mFollowView透明度的比率
            float alphaRatio = 1 - fraction;
            // 設置透明度
            mFollowView.setAlpha(alphaRatio);
            // 根據垂直方向的dy設置top,產生跟隨mFlexView的效果
            mFollowView.setTop(mFollowView.getTop() + dy);

            // 到底部的時候,changedView的top剛好等于mDragHeight,以此作為水平拖動的基準
            mHorizontalDragEnable = top == mDragHeight;

            if (mHorizontalDragEnable) {
                // 如果水平拖動允許的話,由于設置縮放不會影響mFlexView的寬高(比如getWidth),所以水平拖動距離為mFlexView寬度一半
                mDragWidth = (int) (changedView.getMeasuredWidth() * 0.5f);

                // 設置mFlexView的透明度,這里向左右水平拖動透明度都隨之變化
                changedView.setAlpha(1 - Math.abs(left) * 1.0f / mDragWidth);

                // 水平拖動一定距離的話,垂直拖動將被禁止
                mVerticalDragEnable = left < 0 && left >= -mDragWidth * 0.05;

            } else {
                // 不是水平拖動的處理
                changedView.setAlpha(1);
                mDragWidth = 0;

                mVerticalDragEnable = true;

            }

            if (mFlexLayoutPosition == null) {
                // 創建子元素位置緩存
                mFlexLayoutPosition = new ChildLayoutPosition();
                mFollowLayoutPosition = new ChildLayoutPosition();
            }

            // 記錄子元素的位置
            mFlexLayoutPosition.setPosition(mFlexView.getLeft(), mFlexView.getRight(), mFlexView.getTop(), mFlexView.getBottom());
            mFollowLayoutPosition.setPosition(mFollowView.getLeft(), mFollowView.getRight(), mFollowView.getTop(), mFollowView.getBottom());

            //            Log.e("FlexCallback", "225行-onViewPositionChanged(): 【" + mFlexView.getLeft() + ":" + mFlexView.getRight() + ":" + mFlexView.getTop() + ":" + mFlexView
            //                    .getBottom() + "】 【" + mFollowView.getLeft() + ":" + mFollowView.getRight() + ":" + mFollowView.getTop() + ":" + mFollowView.getBottom() + "】");

        }

    }

3.4.接下來是處理測量和定位,我們實現的排列效果類似 LinearLayout 垂直排列的效果,這里被 measureChildWithMargins 的 heightUse 擺了一道;onLayout 的時候在位置緩存不為空的時候直接定位是因為 ViewDragHelper 在處理觸摸事件子元素在做一些平移之類的,若是有元素更新了 UI 會導致重新 Layout,例如我的播放器在更新時間的 TextView 時就會如此,因此在 FlexCallback 的 onViewPositionChanged 方法記錄位置,在重新 Layout 時恢復位置即可,這個也坑了好久

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int desireHeight = 0;
        int desireWidth = 0;

        int tmpHeight = 0;

        if (getChildCount() != 2) {
            throw new IllegalArgumentException("只允許容器添加兩個子View!");
        }

        if (getChildCount() > 0) {
            for (int i = 0; i < getChildCount(); i++) {
                final View child = getChildAt(i);
                // 測量子元素并考慮外邊距
                // 參數heightUse:父容器豎直已經被占用的空間,比如被父容器的其他子 view 所占用的空間;這里我們需要的是子View垂直排列,所以需要設置這個值
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, tmpHeight);
                // 獲取子元素的布局參數
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                // 計算子元素寬度,取子控件最大寬度
                desireWidth = Math.max(desireWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                // 計算子元素高度
                tmpHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
                desireHeight += tmpHeight;
            }
            // 考慮父容器內邊距
            desireWidth += getPaddingLeft() + getPaddingRight();
            desireHeight += getPaddingTop() + getPaddingBottom();
            // 嘗試比較建議最小值和期望值的大小并取大值
            desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
            desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());
        }
        // 設置最終測量值
        setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec), resolveSize(desireHeight, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        if (mFlexLayoutPosition != null) {
            // 因為在用到ViewDragHelper處理布局交互的時候,若是有子View的UI更新導致重新Layout的話,需要我們自己處理ViewDragHelper拖動時子View的位置,否則會導致位置錯誤
            // Log.e("YytLayout1", "292行-onLayout(): " + "自己處理布局位置");
            mFlexView.layout(mFlexLayoutPosition.getLeft(), mFlexLayoutPosition.getTop(), mFlexLayoutPosition.getRight(), mFlexLayoutPosition.getBottom());
            mFollowView.layout(mFollowLayoutPosition.getLeft(), mFollowLayoutPosition.getTop(), mFollowLayoutPosition.getRight(), mFollowLayoutPosition.getBottom());
            return;
        }

        final int paddingLeft = getPaddingLeft();
        final int paddingTop = getPaddingTop();

        int multiHeight = 0;

        int count = getChildCount();

        if (count != 2) {
            throw new IllegalArgumentException("此容器的子元素個數必須為2!");
        }

        for (int i = 0; i < count; i++) {
            // 遍歷子元素并對其進行定位布局
            final View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            int left = paddingLeft + lp.leftMargin;
            int right = child.getMeasuredWidth() + left;

            int top = (i == 0 ? paddingTop : 0) + lp.topMargin + multiHeight;
            int bottom = child.getMeasuredHeight() + top;

            child.layout(left, top, right, bottom);

            multiHeight += (child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
        }

    }

3.5.觸摸事件的處理,由于縮放不會影響 mFlexView 真實寬高,ViewDragHelper 仍然會阻斷 mFlexView 的真實寬高的區域,所以這里判斷手指是否落在 mFlexView 視覺上的范圍內,在才去調 ViewDragHelper 的 shouldInterceptTouchEvent 方法

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        // Log.e("YytLayout", mFlexView.getLeft() + ";" + mFlexView.getTop() + " --- " + ev.getX() + ":" + ev.getY());

       // 由于縮放不會影響mFlexView真實寬高,這里手動計算視覺上的范圍
        float left = mFlexView.getLeft() + mFlexWidth * (1 - mFlexScaleRatio) - mFlexScaleOffset * (1 - mFlexScaleRatio);
        float top = mFlexView.getTop() + mFlexHeight * (1 - mFlexScaleRatio) - mFlexScaleOffset * (1 - mFlexScaleRatio);

       // 這里所做的是判斷手指是否落在mFlexView視覺上的范圍內
        mInFlexViewTouchRange = ev.getX() >= left && ev.getY() >= top;

        if (mInFlexViewTouchRange) {

            return mDragHelper.shouldInterceptTouchEvent(ev);

        } else {
            return super.onInterceptTouchEvent(ev);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mInFlexViewTouchRange) {
            // 這里還要做判斷是因為,即使我不阻斷事件,但是此Layout的子View不消費的話,事件還是給回此Layout
            mDragHelper.processTouchEvent(event);
            return true;
        } else {
            // 不在mFlexView觸摸范圍內,并且子View沒有消費,返回false,把事件傳遞回去
            return false;
        }
    }

3.6.在 computeScroll 中,若是 mIsClosing 為 true,即關閉的整個平移執行完畢了,通知回調事件

@Override
    public void computeScroll() {
        if (mDragHelper.continueSettling(true)) {
            invalidate();
        } else if (mIsClosing && mOnLayoutStateListener != null) {
            // 正在關閉的情況下,并且拖動結束后,告知將要關閉頁面
            mOnLayoutStateListener.onClose();
            mIsClosing = false;
        }
    }

    /**
     * 監聽布局是否水平拖動關閉了
     */
    public interface OnLayoutStateListener {

        void onClose();

    }

    public void setOnLayoutStateListener(OnLayoutStateListener onLayoutStateListener) {
        mOnLayoutStateListener = onLayoutStateListener;
    }

    /**
     * 展開布局
     */
    public void expand() {
        mDragHelper.smoothSlideViewTo(mFlexView, 0, 0);
        invalidate();
    }

3.7.容器實現了,接下來我們繼承 YytLayout 實現播放器頁面的組合控件即可,再封裝一些常用的方法,這里使用的是大名鼎鼎的 Ijkplayer 實現的播放器,屏蔽了 IjkVideoView 的觸摸事件自己處理了;順帶一提,為了實現播放器 Controller 跟隨拖動縮放的效果,放棄了常用的 PopupWindow 實現的思路,IjkController 直接是添加到 IjkVideoView 中的,要不彈窗實現跟隨播放器太麻煩了

/**
 * Created by Oubowu on 2016/12/27 17:32.<p>
 * 仿音悅臺播放頁面的具體實現,組合控件的形式
 */
public class YytPlayer extends YytLayout {

    private IjkController mIjkController;

    private IjkVideoView mIjkVideoView;

    private ImageView mIvAvatar;
    private TextView mTvName;
    private TextView mTvTime;
    private TextView mTvTitle;
    private TextView mTvDesc;
    private RecyclerView mYytRecyclerView;

    private VideoListAdapter mVideoListAdapter;

    public YytPlayer(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {

        // 繼承YytLayout并且通過merge標簽減少層級來實現組合控件
        LayoutInflater.from(context).inflate(R.layout.yyt_player, this, true);

        setOnLayoutStateListener(new OnLayoutStateListener() {

            @Override
            public void onClose() {
                setVisibility(View.INVISIBLE);
                mIjkVideoView.release(true);
            }
        });

        mIjkVideoView = (IjkVideoView) findViewById(R.id.ijk_player_view);
        final int scaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        mIjkVideoView.setOnTouchListener(new OnTouchListener() {

            float mDownX = 0;
            float mDownY = 0;
            boolean mClickCancel;

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                float x = event.getX();
                float y = event.getY();
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        mDownX = x;
                        mDownY = y;
                        break;
                    case MotionEvent.ACTION_MOVE:
                        if (Math.abs(mDownX - x) > scaledTouchSlop || Math.abs(mDownY - y) > scaledTouchSlop) {
                            mClickCancel = true;
                        }
                        break;
                    case MotionEvent.ACTION_UP:
                        if (!mClickCancel && Math.abs(mDownX - x) <= scaledTouchSlop && Math.abs(mDownY - y) <= scaledTouchSlop) {
                            // 點擊事件偶爾失效,只好這里自己解決了
                            if (isHorizontalDragEnable()) {
                                expand();
                            } else {
                                mIjkVideoView.toggleMediaControlsVisibility();
                            }
                        }
                        mClickCancel = false;
                        break;
                    case MotionEvent.ACTION_CANCEL:
                        mClickCancel = false;
                        break;
                }
                return true;
            }
        });

        mIvAvatar = (ImageView) findViewById(R.id.iv_avatar);
        mTvName = (TextView) findViewById(R.id.tv_name);
        mTvTime = (TextView) findViewById(R.id.tv_time);
        mTvTitle = (TextView) findViewById(R.id.tv_title);
        mTvDesc = (TextView) findViewById(R.id.tv_desc);

        mVideoListAdapter = new VideoListAdapter();
        mVideoListAdapter.setOnItemClickCallback(new OnItemClickCallback() {
            @Override
            public void onClick(View view, int position) {
                int pos = (Integer) view.getTag();
                VideoSummary summary = mVideoListAdapter.getData().get(pos);
                playVideo(mVideoListAdapter.getData(), summary);
            }
        });

        mYytRecyclerView = (RecyclerView) findViewById(R.id.yyt_recycler_view);

        GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 2, LinearLayoutManager.VERTICAL, false);
        mYytRecyclerView.setLayoutManager(gridLayoutManager);

        mYytRecyclerView.setNestedScrollingEnabled(false);

        mYytRecyclerView.addItemDecoration(new VideoListItemDecoration(context));

        mYytRecyclerView.setAdapter(mVideoListAdapter);

    }

    // 播放視頻
    private void playVideo(String path, String name) {

        try {
            if (mIjkController == null) {

                IjkMediaPlayer.loadLibrariesOnce(null);
                IjkMediaPlayer.native_profileBegin("libijkplayer.so");

                mIjkController = new IjkController(mIjkVideoView, name);

                mIjkController.setOnViewStateListener(new IjkController.OnViewStateListener() {
                    @Override
                    public void onBackPress() {
                        stop();
                    }
                });

                mIjkVideoView.setMediaController(mIjkController);

                mIjkVideoView.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() {
                    @Override
                    public void onPrepared(IMediaPlayer mp) {
                        mIjkVideoView.start();
                    }
                });

                mIjkVideoView.setOnErrorListener(new IMediaPlayer.OnErrorListener() {
                    @Override
                    public boolean onError(IMediaPlayer mp, int what, int extra) {
                        Toast.makeText(getContext(), "視頻播放出錯了╮(╯Д╰)╭", Toast.LENGTH_SHORT).show();
                        return true;
                    }
                });

            } else {
                // 重新設置視頻名字
                mIjkController.setVideoName(name);
            }

            // 設置這個TextureView播放器縮放就正常了
            mIjkVideoView.setRender(IjkVideoView.RENDER_TEXTURE_VIEW);
            // 因為每次setRender都會移除view再添加,為了縮放效果這里控制器是添加到IjkVideoView中的,所以這里也要重新添加才能在IjkVideoView的最上面
            mIjkController.updateControlView();

            // 顯示加載條
            mIjkController.showProgress();

            // 播放視頻
            mIjkVideoView.setVideoURI(Uri.parse(path));

        } catch (UnsatisfiedLinkError e) {
            e.printStackTrace();
            Toast.makeText(getContext(), "你的CPU是" + Build.CPU_ABI + ",當前播放器使用的編譯版本" + BuildConfig.FLAVOR + "不匹配!", Toast.LENGTH_LONG).show();
        }

    }

    /**
     * 顯示布局,并且播放視頻
     *
     * @param data    視頻列表,用于播放頁面下面的列表布局
     * @param summary 播放的視頻信息
     */
    public void playVideo(List<VideoSummary> data, VideoSummary summary) {

        // 拿到數據,設置到播放的布局的相關信息
        Glide.with(getContext()).load(summary.mTopicImg).transform(new GlideCircleTransform(getContext())).into(mIvAvatar);
        mTvName.setText(summary.mTopicName);
        mTvTime.setText(summary.mPtime);
        mTvTitle.setText(Html.fromHtml(summary.mTitle));
        if (summary.mDescription.isEmpty()) {
            mTvDesc.setText(summary.mTopicDesc);
        } else {
            mTvDesc.setText(Html.fromHtml(summary.mDescription));
        }

        // 設置YytLayout可見,并且展開
        setVisibility(View.VISIBLE);
        expand();

        mVideoListAdapter.setData(data);
        mVideoListAdapter.setItemWidth(mYytRecyclerView.getWidth() / 2);
        mVideoListAdapter.notifyDataSetChanged();

        // 播放視頻
        playVideo(summary.mMp4HdUrl == null ? summary.mMp4Url : summary.mMp4HdUrl, summary.mTitle);
    }

    // 開始播放
    public void start() {
        if (mIjkVideoView != null && !mIjkVideoView.isPlaying()) {
            mIjkVideoView.start();
        }
    }

    // 暫停播放
    public void pause() {
        if (mIjkVideoView != null && mIjkVideoView.isPlaying()) {
            mIjkVideoView.pause();
        }
    }

    // 停止播放
    public void stop() {
        setVisibility(View.INVISIBLE);
        if (mIjkVideoView != null) {
            mIjkVideoView.release(true);
        }
    }

    public boolean isShowing() {
        return getVisibility() == VISIBLE;
    }
}

4.總結

說難也不難,就是各種摳細節需要腦洞,各位不妨看到好玩的交互自己打開腦洞一下,接下來可能要實現下 UC 瀏覽器播放器的效果,感覺也是非常有意思。

 

 

 本文由用戶 HannahMoll 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!