视频播放技术汇总(列表播放,小窗播放,跨界面播放,播放中网络切换提示)

序言

最近的项目中涉及到视频播放,在这里我把关于视频播放技术中的一些心得体会记录下来。

功能

完整演示

这里写图片描述

安装地址

http://pre.im/lNm8

这里写图片描述

基本功能

1.在无wifi的情况下提示用户,包括正在播放的时候网络切换也会提示用户。

这里写图片描述

2.小窗播放:当用户正在观看的视频没有播完,用户又滑动到其他页面则视频继续在小窗播放,播放完成以后小窗自动消失,并提示用户播放完毕。

这里写图片描述

播放完毕提示

这里写图片描述

3.列表播放:支持在列表中播放

这里写图片描述

4.跨界面播放,在列表中播放时,点击列表进入详情页。或在小窗播放时点击小窗进入详情页。视频将继续播放,不会重头开始。

实现

关于视频在任意位置播放,我主要是通过一个VideoPlayManager来管理的。在VideoPlayManager中有一个用来播放视频的VideoPlayView,而在需要播放视频的时候通过Rxbus发送一个事件,事件包含了能够展示VideoPlayView的FragmeLayout和需要播放的视频资源。VideoPlayManager初始化的时候开启了一个线程用来检测当前视频需要播放的位置。

package com.zhuguohui.videodemo.video;

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import com.trs.videolist.CustomMediaContoller;
import com.trs.videolist.VideoPlayView;
import com.zhuguohui.videodemo.R;
import com.zhuguohui.videodemo.activity.FullscreenActivity;
import com.zhuguohui.videodemo.adapter.VideoAdapter;
import com.zhuguohui.videodemo.bean.VideoItem;
import com.zhuguohui.videodemo.rx.RxBus;
import com.zhuguohui.videodemo.service.NetworkStateService;
import com.zhuguohui.videodemo.util.AppUtil;
import com.zhuguohui.videodemo.util.ToastUtil;

import tv.danmaku.ijk.media.player.IMediaPlayer;

import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;


/**
 * 用于管理视频播放的工具类
 * <p>
 * 通过RxBus发送事件来播放和切换播放容器
 * 在程序运行期间通过displayThread自动在小窗模式,列表模式切换。
 * <p>
 * Created by zhuguohui on 2017/1/11 0011.
 */

public class VideoPlayManager {

    private static WindowManager windowManager;
    private static Context sContext;
    private static boolean haveInit = false;

    //小窗播放
    private static FrameLayout smallPlayHolder;
    private static RelativeLayout smallWindow;
    private static LayoutParams smallWindowParams;
    //小窗关闭的button
    private static ImageView iv_close;


    private static VideoPlayView sVideoPlayView;
    //正在播放的Item
    private static VideoItem sPlayingItem = null;
    //正在暂时视频的容器
    private static ViewGroup sPlayingHolder = null;
    //当前的Activity
    private static Activity currentActivity;

    //标识是否在后台运行
    private static boolean runOnBack = false;

    //用于播放完成的监听器
    private static CompletionListener completionListener = new CompletionListener();


    //标识是否在小窗模式
    private static boolean sPlayInSmallWindowMode = false;

    //用于在主线程中更新UI
    private static Handler handler = new Handler(Looper.getMainLooper());

    //记录在小窗中按下的位置
    private static float xDownInSmallWindow, yDownInSmallWindow;

    //记录在小窗中上一次触摸的位置
    private static float lastX, lastY = 0;

    private static VideoAdapter.VideoClickListener videoClickListener = new VideoAdapter.VideoClickListener();


    public static void init(Context context) {
        if (haveInit) {
            return;
        }
        sContext = context.getApplicationContext();
        windowManager = (WindowManager) sContext.getSystemService(Context.WINDOW_SERVICE);
        //初始化播放容器
        initVideoPlayView();
        //创建小窗播放容器
        createSmallWindow();
        //注册事件 处理
        registerEvent();
        Application application = (Application) sContext;
        //监听应用前后台的切换
        application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
        haveInit = true;
    }


    /**
     * 初始化播放控件
     */
    private static void initVideoPlayView() {
        sVideoPlayView = new VideoPlayView(sContext);
        sVideoPlayView.setCompletionListener(completionListener);
        sVideoPlayView.setFullScreenChangeListener(fullScreenChangeListener);
        sVideoPlayView.setOnErrorListener(onErrorListener);

    }

    private static IMediaPlayer.OnErrorListener onErrorListener = (mp, what, extra) -> {
        ToastUtil.getInstance().showToast("播放失败");
        completionListener.completion(null);
        return true;
    };

    /**
     * 用于显示视频的线程
     * 在应用进入前台的时候启动,在切换到后台的时候停止
     * 负责,判断当前的显示状态并显示到正确位置
     */
    private static void createSmallWindow() {
        smallWindow = (RelativeLayout) View.inflate(sContext, R.layout.view_small_holder, null);
        smallPlayHolder = (FrameLayout) smallWindow.findViewById(R.id.small_holder);
        //关闭button
        iv_close = (ImageView) smallWindow.findViewById(R.id.iv_close);
        iv_close.setOnClickListener(v ->
        {
            if (sVideoPlayView.isPlay()) {
                sVideoPlayView.stop();
                sVideoPlayView.release();
            }
            completionListener.completion(null);
        });
        smallWindowParams = new LayoutParams();
        int width = AppUtil.dip2px(sContext, 160);
        int height = AppUtil.dip2px(sContext, 90);
        smallWindowParams.width = width;
        smallWindowParams.height = height;
        smallWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
        smallWindowParams.x = 0;
        smallWindowParams.y = 0;
      /*  if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
            smallWindowParams.type = LayoutParams.TYPE_TOAST;
        } else {
            smallWindowParams.type = LayoutParams.TYPE_PHONE;
        }*/
        smallWindowParams.type = LayoutParams.TYPE_SYSTEM_ERROR;
        smallWindowParams.flags = FLAG_NOT_FOCUSABLE | FLAG_KEEP_SCREEN_ON;
        // 设置期望的bitmap格式
        smallWindowParams.format = PixelFormat.RGBA_8888;
        //实现view可拖动
        smallWindow.setOnTouchListener((v, event) -> {

            switch (event.getAction()) {
                case ACTION_DOWN:
                    xDownInSmallWindow = event.getRawX();
                    yDownInSmallWindow = event.getRawY();
                    lastX = xDownInSmallWindow;
                    lastY = yDownInSmallWindow;
                    break;
                case ACTION_MOVE:
                    float moveX = event.getRawX() - lastX;
                    float moveY = event.getRawY() - lastY;
                    lastX = event.getRawX();
                    lastY = event.getRawY();
                    if (Math.abs(moveX) > 10 || Math.abs(moveY) > 10) {
                        //更新
                        smallWindowParams.x += moveX;
                        smallWindowParams.y += moveY;
                        windowManager.updateViewLayout(smallWindow, smallWindowParams);
                        return true;
                    }
                    break;
                case ACTION_UP:
                    moveX = event.getRawX() - xDownInSmallWindow;
                    moveY = event.getRawY() - yDownInSmallWindow;
                    //实现点击事件
                    if (Math.abs(moveX) < 10 && Math.abs(moveY) < 10) {
                        videoClickListener.onVideoClick(currentActivity, sPlayingItem);
                        return true;
                    }
                    break;
            }
            return false;
        });
    }


    /**
     * 请求用户给予悬浮窗的权限
     */
    public static boolean askForPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (!Settings.canDrawOverlays(currentActivity)) {
                //   Toast.makeText(TestFloatWinActivity.this, "当前无权限,请授权!", Toast.LENGTH_SHORT).show();

                Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                        Uri.parse("package:" + currentActivity.getPackageName()));
//                currentActivity.startActivityForResult(intent,OVERLAY_PERMISSION_REQ_CODE);
                currentActivity.startActivity(intent);
                return false;
            } else {
                return true;
            }
        }
        return true;
    }


    /**
     * 用于监控应用前后台的切换
     */
    private static Application.ActivityLifecycleCallbacks lifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
        private int count = 0;
        private boolean videoPause = false;

        @Override
        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

        }

        @Override
        public void onActivityStarted(Activity activity) {
            if (count == 0) {
                //切换到前台
                runOnBack = false;
                if (sPlayInSmallWindowMode) {
                    windowManager.addView(smallWindow, smallWindowParams);
                }
                //继续播放视频
                if (videoPause) {
                    sVideoPlayView.pause();
                    videoPause = false;
                }
                DisPlayThread.startDisplay();
            }
            count++;
        }

        @Override
        public void onActivityResumed(Activity activity) {
            currentActivity = activity;
        }

        @Override
        public void onActivityPaused(Activity activity) {

        }

        @Override
        public void onActivityStopped(Activity activity) {
            count--;
            if (count == 0) {
                //切换到后台
                runOnBack = true;
                //停止检测线程
                DisPlayThread.stopDisplay();
                //如果是小窗模式移除window
                if (sPlayInSmallWindowMode) {
                    windowManager.removeView(smallWindow);
                }

                //视频暂停
                if (sVideoPlayView.isPlay()) {
                    sVideoPlayView.pause();
                    videoPause = true;
                }

            }
        }

        @Override
        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

        }

        @Override
        public void onActivityDestroyed(Activity activity) {

        }
    };

    /**
     * 退出全屏
     */
    private static void exitFromFullScreenMode() {
        currentActivity.finish();
    }

    private static CustomMediaContoller.FullScreenChangeListener fullScreenChangeListener = () -> {
        if (!(currentActivity instanceof FullscreenActivity)) {
            enterFullScreenMode();
        } else {
            exitFromFullScreenMode();
        }
    };


    private static void enterFullScreenMode() {
        currentActivity.startActivity(new Intent(currentActivity, FullscreenActivity.class));
    }


    private static class CompletionListener implements VideoPlayView.CompletionListener {

        @Override
        public void completion(IMediaPlayer mp) {

            if (currentActivity instanceof FullscreenActivity) {
                currentActivity.finish();
            }

            //如果是小窗播放则退出小窗
            if (sPlayInSmallWindowMode) {
                if (mp != null) {
                    //mp不等于null表示正常的播放完成退出
                    //在小窗消失之前给用户一个提示消息,防止太突兀
                    ToastUtil.getInstance().ok().showToast("播放完毕");
                }
                exitFromSmallWindowMode();
            }

            //将播放控件从器父View中移出
            removeVideoPlayViewFromParent();

            sPlayingItem = null;
            if (sPlayingHolder != null) {
                sPlayingHolder.setKeepScreenOn(false);
            }
            sPlayingHolder = null;
            //释放资源
            sVideoPlayView.release();
        }

    }

    /**
     * 注册事件处理
     */
    private static void registerEvent() {

        //处理在View中播放
        RxBus.getDefault().toObserverable(PlayInViewEvent.class).subscribe(playInViewEvent -> {


            //表示播放容器,和视频内容是否变化
            boolean layoutChange = sPlayingHolder == null || !sPlayingHolder.equals(playInViewEvent.getPlayLayout());
            boolean videoChange = sPlayingItem == null || !sPlayingItem.equals(playInViewEvent.getNewsItem());


            //重置状态,保存播放的Holder
            if (videoChange) {
                sPlayingItem = playInViewEvent.getNewsItem();

            }

            if (layoutChange) {
                removeVideoPlayViewFromParent();
                if (sPlayingHolder != null) {
                    //关闭之前View的屏幕常亮
                    sPlayingHolder.setKeepScreenOn(false);
                }
                sPlayingHolder = playInViewEvent.getPlayLayout();
                //将播放的Item设置为播放view的tag,就可以通过displayThread检查当前Activity中是否
                //包含了这个tag的View存在,而直到是否有播放容器存在,如果没有的话就使用小窗播放。
                sPlayingHolder.setTag(sPlayingItem);
                //显示控制条
                sVideoPlayView.setShowContoller(true);
                //开启屏幕常亮
                sVideoPlayView.setKeepScreenOn(true);
                sPlayingHolder.addView(sVideoPlayView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
            }

            if (videoChange) {
                //播放新视频
                if (sVideoPlayView.isPlay()) {
                    sVideoPlayView.stop();
                    sVideoPlayView.release();
                }
                sPlayingHolder.setTag(sPlayingItem);

                //判断网络,如果在移动网络则提示用户
                ViedoPlayChecker.checkPlayNet(currentActivity, () -> {
                    sVideoPlayView.start(sPlayingItem.getVideoUrl());
                }, () -> {
                    completionListener.completion(null);
                });

            } else {
                //重播
                if (!sVideoPlayView.isPlay()) {
                    sVideoPlayView.start(sPlayingItem.getVideoUrl());
                }
            }
        });

        //处理视频回退
        RxBus.getDefault().toObserverable(PlayVideoBackEvent.class).subscribe(playVideoBackEvent -> {
            sPlayingHolder = null;
        });

        //处理网络变化
        RxBus.getDefault().toObserverable(NetworkStateService.NetStateChangeEvent.class).subscribe(netStateChangeEvent -> {
            if (netStateChangeEvent.getState() == NetworkStateService.NetStateChangeEvent.NetState.NET_4G && sVideoPlayView.isPlay()) {
                sVideoPlayView.pause();
                //如果在移动网络播放,则提示用户
                ViedoPlayChecker.checkPlayNet(currentActivity, () -> {
                    sVideoPlayView.pause();
                }, () -> {
                    completionListener.completion(null);
                });
            }
        });

        //处理取消播放事件
        RxBus.getDefault().toObserverable(PlayCancleEvent.class).subscribe(playCancleEvent -> {
            completionListener.completion(null);
        });

    }


    /**
     * 进入小窗播放模式
     */
    private static void enterSmallWindowMode() {
        //检查权限
        if (!askForPermission()) {
            ToastUtil.getInstance().showToast("小窗播放需要浮窗权限");
            return;
        }

        if (!sPlayInSmallWindowMode) {
            handler.post(() -> {
                removeVideoPlayViewFromParent();
                //隐藏控制条
                sVideoPlayView.setShowContoller(false);
                smallPlayHolder.addView(sVideoPlayView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                try {
                    windowManager.addView(smallWindow, smallWindowParams);
                } catch (Exception e) {
                    e.printStackTrace();
                    //已经添加了,则更新
                    windowManager.updateViewLayout(smallWindow, smallWindowParams);
                }
                sPlayingHolder = smallPlayHolder;
                sPlayInSmallWindowMode = true;
            });
        }
    }


    /**
     * 退出小窗播放模式
     */
    private static void exitFromSmallWindowMode() {
        if (sPlayInSmallWindowMode) {
            handler.post(() -> {
                windowManager.removeView(smallWindow);
                sPlayInSmallWindowMode = false;
                //显示控制条
                sVideoPlayView.setShowContoller(true);
            });
        }
    }


    private static void removeVideoPlayViewFromParent() {
        if (sVideoPlayView != null) {
            if (sVideoPlayView.getParent() != null) {
                ViewGroup parent = (ViewGroup) sVideoPlayView.getParent();
                parent.removeView(sVideoPlayView);
            }
        }
    }

    public static class DisPlayThread extends Thread {
        private boolean check = false;

        private static DisPlayThread disPlayThread;

        public synchronized static void startDisplay() {
            if (disPlayThread != null) {
                stopDisplay();
            }
            disPlayThread = new DisPlayThread();
            disPlayThread.start();
        }

        public synchronized static void stopDisplay() {
            if (disPlayThread != null) {
                disPlayThread.cancel();
                disPlayThread = null;
            }
        }

        private void cancel() {
            check = false;
        }

        private DisPlayThread() {
        }


        @Override
        public void run() {
            while (check) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //如果在后台运行,直接退出
                if (runOnBack) {
                    check = false;
                    stopDisplay();
                    return;
                }

                //检查是否有正在播放的Item,如果没有则不显示任何播放界面
                if (sPlayingItem == null) {
                    continue;
                }

                //检查是否有可播放的容器,通过Tag查找,不能通过id查找
                //因为在ListView或者RecycleView中View是会复用的,因此需要在ListView,或RecycleView中每次
                //创建holder的时候把tag设置到需要展示Video的FrameLayout上。
                //使用正在播放的item作为tag;
                if (currentActivity != null) {
                    View contentView = currentActivity.findViewById(android.R.id.content);
                    View playView = contentView.findViewWithTag(sPlayingItem);

                    //判断正在播放的view是否是显示在界面的,在ListView或RecycleView中会有移除屏幕的情况发生
                    if (isShowInWindow(playView)) {
                        //如果显示,判断是否和之前显示的是否是同一个View
                        //如果不是则切换到当前view中
                        exitFromSmallWindowMode();
                        if (sPlayingHolder != playView) {
                            handler.post(() -> {
                                //关闭屏幕常亮
                                if (sPlayingHolder != null) {
                                    sPlayingHolder.setKeepScreenOn(false);
                                }
                                removeVideoPlayViewFromParent();
                                ViewGroup viewGroup = (ViewGroup) playView;
                                viewGroup.addView(sVideoPlayView, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
                                sPlayingHolder = viewGroup;
                                //保持屏幕常亮
                                sPlayingHolder.setKeepScreenOn(true);
                            });

                        }
                    } else {
                        //如果不显示,则在小窗中播放
                        enterSmallWindowMode();
                    }
                }
            }
        }

        Rect r = new Rect();

        private boolean isShowInWindow(View view) {
            if (view == null) {
                return false;
            }
            boolean localVisibleRect = view.getLocalVisibleRect(r);
            boolean show = localVisibleRect && view.isShown();
            return show;
        }

        @Override
        public synchronized void start() {
            check = true;
            super.start();
        }


    }

    public static VideoItem getPlayingItem() {
        return sPlayingItem;
    }


    /**
     * 取消播放事件,比如应用程序退出时发出这个时间
     */
    public static class PlayCancleEvent {
    }

    /**
     * 视频播放退出
     */
    public static class PlayVideoBackEvent {
    }

    /**
     * 将视频显示在指定的View中
     * 如果视频发生改变则播放视频
     * 如果view发生改变但是视频没有改变,则只是切换播放的view。
     */
    public static class PlayInViewEvent {
        FrameLayout playLayout;
        VideoItem newsItem;
        boolean playInList;

        public PlayInViewEvent(FrameLayout playLayout, VideoItem newsItem) {
            this(playLayout, newsItem, false);
        }

        public PlayInViewEvent(FrameLayout playLayout, VideoItem newsItem, boolean playInList) {
            this.playLayout = playLayout;
            this.newsItem = newsItem;
            this.playInList = playInList;
        }

        public VideoItem getNewsItem() {
            return newsItem;
        }

        public void setNewsItem(VideoItem newsItem) {
            this.newsItem = newsItem;
        }

        public FrameLayout getPlayLayout() {
            return playLayout;
        }

        public void setPlayLayout(FrameLayout playLayout) {
            this.playLayout = playLayout;
        }
    }
}

视频播放的时候只需要发送一个消息就行了。

   RxBus.getDefault().post(new VideoPlayManager.PlayInViewEvent(holder.layout_holder, videoItem, true));

需要注意的时候,为了能在ListView和RecyclerView中播放,需要将播放的item绑定的播放容器上,这样在线程检测当前界面是否有能播放视频的容器时才不会因为RecyclerView的复用而出错。

     holder.layout_holder.setTag(videoItem);

关于更多的细节大家看我的Demo吧,内容实在太多。

Demo

https://github.com/zhuguohui/VideoDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 最近做了一个Android UI相关开源项目库汇总,里面集合了OpenDigg 上的优质的Android开源项目库...
    OpenDigg阅读 17,044评论 6 223
  • 真的是忙于交际,没有太多精力学习。连写总结的欲望都没有,今天学了朱伟的恋恋有词,明天有机会继续吧。明天应该早点起,...
    muziyue阅读 222评论 0 0
  • 什么是区块链? 区块链的英文是Block Chain,由Block和Chain组成,顾名思义,它应该包含两方面的意...
    杨何阅读 649评论 0 0
  • 今天特地選了一個自己完全不了解的領域書籍:行為心理學讀起來特別燒腦,他們把"人"稱為"有機體"或"個體",各種行為...
    品兴阅读 239评论 0 0