基于 SurfaceView 详解 android 幸运大转盘,附带实例app

首先说一下,幸运大转盘,以及SurfaceView是在看了也为大神的博客,才有了比较深刻的理解,当然这里附上这位大神的博客地址:博客地址,有兴趣的话你可以去看看,里面有很多的例子。至于我为什么要写这篇博客?,原因之一:加强自己的理解,原因之二:大神的博客就是大神的博客,跳转的太快,基础不好的,很难理解。还有就是一天在实验室太无聊了,没事写写东西。这里我再来更加基础的分析一下。写的不好,原谅。有什么写的不对的地方还望指出,谢谢。附上kensoon918@163.com.有意者交流交流。接下来切入正题,我们来详细解说一下这个大转盘。

1.首先附上效果图以及简单的分析

这个效果图,是不是很眼熟,当然这个就是最后的效果图,货真价实的,一点不偏差,还比这个流畅。因为这个实在虚拟机上面截取的,你也知道虚拟机的流畅度和真机是没有办法比的。

1.首先分析一下,做这个大转盘,都需要实现什么。

不难看出做这个大转盘需要两个控件,一个是自定义的SurfaceView,还有一个当然就是中间的永远不动的按钮指针,每次点击只是换一下图片就行了。

2.说一下实现这个的基本思路。

实现这个看起来是不是很难,当然对于一些大神级别的人物来说,这个就是小菜一碟,但是关键是大多数还不是。咋眼一看,不就是一个盘子在不停的转么。中间多了一个控制盘子的按钮。要实现盘子不停的旋转就得靠这个SurfaceView了,查阅官方ApiSurfaceView直接直接父类是View,SurfaceView与其他的View有一个重要的却别,那就是SurfaceView允许非UI线程修改,这个也上市SurfaceView的一大优点吧, 这样就不用用一个View每次还得通过非UI线程去通知UI线程修改视图,多麻烦啊。    所以通过,SurfaceView你就可以开一个线程在后台不断的更新UI,方便多了。、

2.SurfaceView的常用模板

虽然SurfaceView很方便做一个小游戏,但是也不随便就乱来,网上有个比较常用的模板,相信这样做既能打到你的目的又能事半功倍,何乐而不为啊。

public class SurfaceViewTemplate extends SurfaceView implements Callback,Runnable{//定义一个SurfaceHolder 用于接受获取的SurfaceHolderprivate    SurfaceHolder mHolder;//用于获取绑定mHOlder的Canvasprivate Canvas mCanvas;//用于不断绘图的线程private Thread mThread;//用于控制线程的开关private boolean isRunning;//这个时代三个参数的构造函数,一般自会在有使用自定义属性的时候才会调用这个构造函数/*** @param context 上下文* @param attrs      xml文件定义的属性* @param defStyle    自定义属性*/public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}//当时用xml文件定义这个自定义View的时候,就会调用这个带两个参数的构造函数public SurfaceViewTemplate(Context context, AttributeSet attrs) {super(context, attrs);//获取SurfaceHoldermHolder=getHolder();//添加callbackmHolder.addCallback(this);//设置一些属性,焦点,屏幕常亮setFocusable(true);setFocusableInTouchMode(true);setKeepScreenOn(true);}//当是在代码里面显示的定义的时候就会调用这个带一个参数的构造函数public SurfaceViewTemplate(Context context) {//在这里我们让他去调用带两个参数的构造函数,以便就算是在代码里面定义的也能完成一些初始化操作this(context,null);}//主要,也是最核心的工作都是在run方法里面执行的,如draw()@Overridepublic void run() {try{//这里通过死循环,不断的进行绘图,给你一种盘在不断旋转的错觉while (isRunning){draw();}}catch(Exception e){e.printStackTrace();}}@Overridepublic void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {}//在这里做一些初始化的工作,开启线程。。。@Overridepublic void surfaceCreated(SurfaceHolder arg0) {//实例化线程,并设置isRunningisRunning=true;mThread=new Thread(this);mThread.start();}//当SurfaceView执行destroy的时候关闭线程@Overridepublic void surfaceDestroyed(SurfaceHolder arg0) {//关闭线程只需设置isRunningisRunning=false;}//这里制定以下控件的宽高@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);}

3.接下来就根据大转盘,定义成员变量

不得不说,成员变量确实有点多,出去模板给的几个外,还得定义,文本,图片,图片地址,各种。。。有点记不住,附上代码

// surfaceprivate SurfaceHolder mHolder;// 与surface绑定在一起的Canvasprivate Canvas mCanvas;// 用于绘制的线程private Thread mThread;// 线程的控制开关private boolean isRunning;// 描述抽奖的文字private String[] mName = new String[] { "单反相机", "IPAD", "手气不好", "IPHONE","张杰一枚", "手气不好" };// 每块的颜色private int deepColor = 0xFFFFC300;private int lightColor = 0xFFF17E01;private int[] mColors = new int[] { deepColor, lightColor, deepColor,lightColor, deepColor, lightColor, };// 与文字对应的图片private int[] mImgs = new int[] { R.drawable.danfan, R.drawable.ipad,R.drawable.f040, R.drawable.iphone, R.drawable.meizi,R.drawable.f040 };// 与文字对应的图片的数组private Bitmap[] mImgsBitmap;// 盘块的个数private final int mItemCount = 6;// 绘制盘块的范围private RectF mRange = new RectF();// 圆的直径private int mRadius;// 绘制盘块的画笔private Paint mArcPaint;// 绘制文字的画笔private Paint mTextPaint;// 滚动的速度private double mSpeed;private volatile float mStartAngle = 0;// 递减的加速度private int aSpeed = 1;// 是否点击了停止private boolean isShouldEnd;// 控件的中心位置private int mCenter;// 控件的paddingprivate int mPadding;// 背景图片private Bitmap mBgBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.bg2);// 文字的大小private float mTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics());public LuckyPadView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}

这些基本上都是不可少的,当然你可以简化一些,例如,减速的家速度,以及你已经显示的知道了盘的块数就是6个,每次用的时候写六个就行。

4.编写构造方法

这个构造方法嘛,有多重编写方式,看自己的用途如果只是在xml文件里面定义,且不带自定义属性的时候,就还可以忽略一个和三个参数的构造函数直接编写两个参数的构造函数,当然为了兼容性,建议三个构造函数都支持,以免不必要的麻烦嘛,照例附上代码:

//做一些初始化的操作public LuckyPadView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);// 获得holder,和与之关联的CanvansmHolder = getHolder();mHolder.addCallback(this);// 设置可获得焦点,以及常亮setFocusable(true);setFocusableInTouchMode(true);setKeepScreenOn(true);}//去调用三个参数的构造函数public LuckyPadView(Context context, AttributeSet attrs) {this(context,null,0);}// 一个参数的构造函数去调用两个构造参数的构造函数public LuckyPadView(Context context) {this(context, null);}

5.编写onMeasure 方法

写自定义控件的时候,不能忽略的一个方法,他指定了自己的大小,以及子控件最大的大小。。。不多说,有一点值得说一下,就是控件的大小是以宽高当中最小的为为基准这样做的目的是为了让控件能成一个正方形,方便以后绘制圆形,以确定半径,中心等,所以在xml文件里面定义的时候别忘了centerInParent。废话不多说,附上源码:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// 获得宽高当中最小的int width = getMeasuredWidth();int height = getMeasuredHeight();int min = width < height ? width : height;// 获得圆的直径mRadius = min - getPaddingLeft() - getPaddingRight();// 获得padding值,一paddingleft为基准mPadding = getPaddingLeft();// 设置中心点mCenter = min / 2;setMeasuredDimension(min, min);}

6.编写 surfaceCreated 顺便附上 surfaceDestroyed

这两个方法的作用,见名知意。在create的时候我们需要实例化线程,并且将线程开启,以及初始化一些成员变量,如:mRange确定绘图区域,附上一张图就理解了。在destroy的时候我们必须把线程关闭不然就会造成一个严重的后果,内存泄露哦。附上代码:

// 做一些初始化的工作@Overridepublic void surfaceCreated(SurfaceHolder arg0) {// 初始化绘制圆弧的画笔,并设置锯齿之类的mArcPaint = new Paint();mArcPaint.setAntiAlias(true);mArcPaint.setDither(true);// 初始化绘制文字的画笔mTextPaint = new Paint();mTextPaint.setColor(0xFFffffff);mTextPaint.setTextSize(mTextSize);// 圆弧的绘制范围,绘制的范围刚好是一个正方形,这个我得做一个插图(1),不然理解不了mRange = new RectF(mPadding, mPadding, mRadius + mPadding, mRadius+ mPadding);// 初始化图片mImgsBitmap = new Bitmap[mItemCount];for (int i = 0; i < mItemCount; i++) {mImgsBitmap[i] = BitmapFactory.decodeResource(getResources(),mImgs[i]);}// 开启线程isRunning = true;mThread = new Thread(this);mThread.start();}// 主要是用来关闭线程的@Overridepublic void surfaceDestroyed(SurfaceHolder arg0) {// 通知线程关闭isRunning = false;}

7.重头戏 run方法

基本上所有的操作都是在这个run方法里面执行的,当然了为了代码的可阅读行,我们打run方法里面的各种操作,打包成方法放在了外面。主要打包的方法draw(),绘图操作。通过一个死循环,不断的进行绘图。给用户一种转盘在不断的旋转的感觉。但是由于绘图是由计算机执行的,所以你是不知道他的执行时间是多少,而且计算机在某一时刻的性能也是不确定的,所以就会造成绘图的时间有差异,这样不就造成了假速度时快时慢这样可不行。所以有一个很巧妙的处理方法,就是记录你绘图的时间。然后你给一个绘图的标准时间值,当小于这个绘图时间的时候,就让线程休眠不足的时间,这样就很好的解决了。附上代码:

@Overridepublic void run() {// 不断地进行绘图,这样就给你一个错觉,转盘在不停的转while (isRunning) {// 这一次开始绘图的时间long start = System.currentTimeMillis();// 真正的绘图操作draw();// 这一次绘图的结束时间long end = System.currentTimeMillis();// 如果你的手机太快,绘图分分钟的事情,那也得让他把那个50等完try {if (end - start < 50) {Thread.sleep(50 - (end - start));}} catch (Exception e) {e.printStackTrace();}}}

8.run() 方法里面的 draw()

在这个draw()方法里面工作就来了,你首先需要获得与mHolder绑定的Canvas,然后绘制背景,一次绘制每个块的背景,然后再绘制块上面的文字,每个快上面的图片。。。当然每个快的位置是根据mStartAngle和速度以及sweepAngle,tmpAngle计算出来的,还得判断用户是否摁下了停止按钮,如果是,得按一定的加速度减速。当然速度不能小于零所以最后还得判断一下,以便将速度回执到0。对了知道你有没有注意到,各种try{}catch(){},这个是为了防止不知什么时候来个异常之类的,我可不希望我的转盘半路crash了,附上代码:

// 绘图private void draw() {try {mCanvas = mHolder.lockCanvas();if (mCanvas != null) {// 首先绘制背景图drawBg();// 绘制每个弧形,以及每个弧形上的文字,以及每个弧形上的图片float tmpAngle = mStartAngle;float sweepAngle = (float) (360 / mItemCount);for (int i = 0; i < mItemCount; i++) {// 这个就是传说中的,背景颜色mArcPaint.setColor(mColors[i]);// 这里真的画了一个扇形出来,干脆我在这里也弄一个插图算了(4)详细可以参见// http://blog.sina.com.cn/s/blog_783ede0301012im3.html// oval :指定圆弧的外轮廓矩形区域。// startAngle: 圆弧起始角度,单位为度。// sweepAngle: 圆弧扫过的角度,顺时针方向,单位为度。// useCenter: 如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形。// paint: 绘制圆弧的画板属性,如颜色,是否填充等。mCanvas.drawArc(mRange, tmpAngle, sweepAngle, true,mArcPaint);// 绘制文本drawText(tmpAngle, sweepAngle, mName[i]);// 绘制IcondrawIcon(tmpAngle, i);// 转换角度,不能再一个地方一直绘制,tmpAngle += sweepAngle;}// 当mspeed不等于0时,相当于滚动mStartAngle += mSpeed;// 当点击停止时,设置mspeed慢慢递减,而不是一下就停了下来if (isShouldEnd) {mSpeed -= aSpeed;}// mspeed小于0的时候就该停止了if (mSpeed < 0) {mSpeed = 0;isShouldEnd = false;}// 根据当前旋转的mStartAngle计算当前滚动的区域callInExactArea(mStartAngle);}} catch (Exception e) {e.printStackTrace();} finally {if (mCanvas != null)mHolder.unlockCanvasAndPost(mCanvas);}}

这里面有一个绘制每一个块的背景,插图理解一下

mCanvas.drawArc(mRange, tmpAngle, sweepAngle, true,mArcPaint);

9. draw()方法里面的drawBg()

首先得附上一张图片不然看不懂啊,通过这个图相信你能够非常清楚的看清楚接下来的代码了,无外乎就是在mRange的外面绘制了一圈,用于美观的背景。作用不大,不多说,附上代码:

// 绘制背景图private void drawBg() {// 根据当前旋转的mStartAngle计算当前滚动到的区域 绘制背景,不重要,完全为了美观mCanvas.drawColor(0xFFFFFFFF);// 这里这个绘图一般又看不懂了,得有个插图才行,这个貌似比圆弧的绘制范围大了那么一圈mCanvas.drawBitmap(mBgBitmap, null, new Rect(mPadding / 2,mPadding / 2, getMeasuredWidth() - mPadding / 2,getMeasuredWidth() - mPadding / 2), null);}

10.draw()里的drawText()方法,用于绘制块里面的文本

首先上一张图,绘制这个文本确实有点麻烦,毕竟文本的位置有点特殊。

绘制这个文字我也是理解了很久,真是有点绕。绘制这个文字是通过path先确定一个Arc,为一个弧形,然后通过水平和垂直偏移量共同定位文字的最终位置,图中有标示。float hOffset = (float) (mRadius * Math.PI / mItemCount / 2 - textWidth / 2);解释一下这个公式首先得到弧长,然后减去文字宽度的一半就得到了水平偏移量,垂直偏移量就跟好理解了直接是float vOffset = (float) (mRadius / 2 / 6);也就是半径的1/6 附上代码:

/*** 绘制文本* * @param startAngle* @param sweepAngle* @param mName2*/private void drawText(float startAngle, float sweepAngle, String mName2) {// pathPath path = new Path();// 将写字区域加上去path.addArc(mRange, startAngle, sweepAngle);// 文字的宽度float textWidth = mTextPaint.measureText(mName2);// 利用水平偏移和垂直偏移让文字居中,是不是理解不了 ,我也是,画个插图,(3)float hOffset = (float) (mRadius * Math.PI / mItemCount / 2 - textWidth / 2);float vOffset = (float) (mRadius / 2 / 6);// 得把文字画上去了mCanvas.drawTextOnPath(mName2, path, hOffset, vOffset, mTextPaint);}

11.draw()方法里面的drawIcon()

首先还是附上一张图片,便于理解,毕竟绘这个图也不是那么容易理解。  图片是不是很详细。要绘制这个图我们首先得确定一个Rect,而这个Rect就是插图阴影的部分,只要确定了这个阴影部分,绘图就很简单了。关键就是确定这个阴影部分。首先我们通过那个平分角和startAngle得到X和Y通过三角函数,别告诉我你忘了。然后通过mCenter加上和减去等等操作得到了,最后的阴影部分,最后附上代码:

/*** 绘制Icon* * @param startAngle* @param i*/private void drawIcon(float startAngle, int i) {// 设置图片的宽度,为直径的1/8,当然可以随便改int imgWidth = mRadius / 8;// 换算成弧度float angle = (float) ((30 + startAngle) * (Math.PI / 180));// x,y ... 这个或许要一张图篇才能理解,(5)int x = (int) (mCenter + mRadius / 2 / 2 * Math.cos(angle));int y = (int) (mCenter + mRadius / 2 / 2 * Math.sin(angle));// 确定绘制图片的位置Rect rect = new Rect(x - imgWidth / 2, y - imgWidth / 2, x + imgWidth/ 2, y + imgWidth / 2);// 绘制mCanvas.drawBitmap(mImgsBitmap[i], null, rect, null);}

12.   luckyStart(index)方法

这个方法,就是显示的在转盘点击开始后,开始转之前根据计算结果设置转盘最后的结果。是不是很想骂一些电商,就知道玩弄我们的感情。结果早就知道了。当然有点难理解,很多公式,先附上一张图:      只要将组后的结果控制在这个210到270的角度范围就行了,至于怎么回事,有公式慢慢理解,附上代码:

/*** 现在总算看穿了,一切电商的阴谋,都是骗人的,电商可以显示的设置你转盘的结果* * @param luckyIndex*/public void luckyStart(int luckyIndex) {// 每一项的角度大小float angle = (float) (360 / mItemCount);// 中奖角度范围,因为指针是朝上的所以范围是在210-270,这里要一个插图才能明白啊(6)float from = 270 - (luckyIndex + 1) * angle;float to = from + angle;// 停下来是旋转的距离float targetFrom = 4 * 360 + from;/** * 这里有点绕,等细细评味*/float v1 = (float) (Math.sqrt(1 * 1 + 8 * targetFrom) - 1) / 2;float targetTo = 4 * 360 + to;float v2 = (float) (Math.sqrt(1 * 1 + 8 * 1 * targetTo) - 1) / 2;mSpeed = (float) (v1 + Math.random() * (v2 - v1));isShouldEnd = false;}

13.SurfaceView 辅助方法和MainActivity代码

这里没有什么贴别的就是对一些方法的调用,在这里我附上一些附加的方法,相信你一看就懂

public void luckyEnd() {mStartAngle = 0;isShouldEnd = true;}public boolean isStart() {return mSpeed != 0;}public boolean isShouldEnd() {return isShouldEnd;}

下面就是MainActivity的代码了

public class MainActivity extends Activity {private LuckyPadView id_luckypadview;private ImageView id_imageview;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initView();setListener();}private void setListener() {id_imageview.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View arg0) {if (!id_luckypadview.isStart()){id_imageview.setImageResource(R.drawable.stop);Random random=new Random();id_luckypadview.luckyStart(random.nextInt()%6);}else {if (!id_luckypadview.isShouldEnd()){id_imageview.setImageResource(R.drawable.start);id_luckypadview.luckyEnd();}}}});}private void initView() {id_luckypadview=(LuckyPadView)findViewById(R.id.id_luckypadview);id_imageview=(ImageView)findViewById(R.id.id_imageview);}

14.最后附上XML里面的代码,很简单,一看就懂

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#ffffff"><com.fat246.view.LuckyPadView android:id="@+id/id_luckypadview"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_centerInParent="true"android:padding="30dp"/><ImageView android:id="@+id/id_imageview"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/start"android:layout_centerInParent="true"/>
</RelativeLayout>

最后我把源码放到了网上,长期有效。

源码地址

转载于:https://my.oschina.net/kensoon/blog/704623

基于 SurfaceView 详解 android 幸运大转盘,附带实例app相关推荐

  1. Jquery写的幸运大转盘抽奖实例,用asp.net处理的服务器逻辑,附源码下载

    [实例简介] 该幸运大转盘抽奖实例已实现服务器端的业务逻辑代码,稍加改动就可以应用实际了 文件:590m.com/f/25127180-488779229-66bbf7(访问密码:551685) [实 ...

  2. Android幸运大转盘

    商品图片的实现: 把所有的商品图片中心点连起来其实就是一个圆:圆的半径根据自己的背景图片来判断具体的, 文字实现: 背景图片不同时,偏移量自行调整 旋转效果: 代码已做详尽注释 源码下载

  3. 免费三加一php源码,最新微联运微信投票独立版PHP源码|基于31CMS投票系统二次开发+幸运大转盘+独立后台吸粉工具...

    源码介绍 投票系统对于微信公众号来说是一种非常有效的吸粉手段!!微信投票系统是基于网络的一种投票收集及统计的系统,比传统的投票统计更为方便.快速.准确.投票的同时 ,会有更多的朋友关注你,朋友的好友通 ...

  4. 基于JavaScript的幸运大转盘

    引言:幸运大转盘在很多网站上.APP都有出现,之前的话一直也没有去琢磨,自从把canvas学了一遍后,什么都想自己做一个,这不就写了个幸运大转盘玩玩,欢迎大家来指导交流! 页面效果: 实现思路 绘制外 ...

  5. 详解 Android 的 Activity 组件

    本文详细介绍了 Android 应用编程中 Activity 的生命周期.通信方式和 Intent Filter 等内容,并提供了一些日常开发中经常用到的关于 Activity 的技巧和方法.通过本文 ...

  6. 必过SafetyNet!以MIUI开发版系统为例详解Android设备通过SafetyNet校验方法

    必过SafetyNet!以MIUI开发版系统为例详解Android设备通过SafetyNet校验方法 作者 梓沐啊_(KylinDemons) 版权声明 Copyright © 2021 KylinD ...

  7. 实现可点击的幸运大转盘

    之前的项目有一个幸运大转盘的功能,在网上找了很久,都没有合适的方法. 这是效果图,实现目标:十二星座的图片可点击切换选中效果,根据选择不同的星座,实现不同的 方法.之前网上的都是带有指针的,或者可点击 ...

  8. android uri图片压缩,详解android 通过uri获取bitmap图片并压缩

    详解android 通过uri获取bitmap图片并压缩 很多人在调用图库选择图片时会在onactivityresult中用media.getbitmap来获取返回的图片,如下: uri mimage ...

  9. 视频教程-Android Studio 开发详解-Android

    Android Studio 开发详解 1999年开始从事开发工作,具备十余年的开发.管理和培训经验. 在无线通信.Android.iOS.HTML5.游戏开发.JavaME.JavaEE.Linux ...

最新文章

  1. 大洋洲群狼来了! 这是中国篮球学习契机?
  2. session的存储方式
  3. 论文浅尝 | Leveraging Knowledge Bases in LSTMs
  4. project日历设置-大小周交替
  5. Hashtable Dictionary的使用
  6. PHP结合Redis来限制用户或者IP某个时间段内访问的次数
  7. yml 后面的配置覆盖前面的
  8. 学技术的不能自废武功
  9. c语言小游戏如何编写,如何用c语言编写小游戏.docx
  10. HTTP网络协议四:HTTP报文及报文字段说明
  11. 英国交通分析指南(Transport analysis guidance)解读及启示
  12. PX4模块设计之九:PX4飞行模式简介
  13. 华为云C6系列服务器,真实评价华为云c6s和c6怎么样-配置区别不大
  14. 通州区机器人比赛活动总结_机器人大赛总结报告
  15. 计算机硬件技术基础 试题与答案,计算机硬件技术基础网上作业及答案
  16. MSP430F149用模拟SPI和FM25CL640通信
  17. Linux 重启nginx服务
  18. jQuery--复制节点clone()详解
  19. Domain Adaptation for Object Detection using SE Adaptors and Center Loss 论文翻译
  20. 计算机管理映像路径,手把手教你解决win7系统任务管理器显示映像路径的恢复办法...

热门文章

  1. android 手写签字
  2. 5.泛型接口:什么是泛型接口???
  3. 治好便秘需养成6个习惯
  4. 分区软件Acronis Disk Director Suite
  5. unity-Fatal Error GC-GetThreadContext Failed
  6. 最新免杀!可过360核晶与Defender(SysWhispers3)
  7. 冰雪覆盖不了的足迹,人间自是有情在
  8. android项目uc浏览器,Android项目仿UC浏览器和360手机卫士消息常驻栏(通知栏)
  9. 为什么说 WebAssembly 属于浏览器之外? Why WebAssembly Belongs Outside the Browser
  10. Python 把图片用文字填充