Android VR Player(全景视频播放器) [6]:视频列表的实现-本地视频

(本篇博客参考《Android第一行代码(第二版)》中关于RecyclerView的部分)

列表的实现方式

列表一般使用Listview来实现,但是Listview使用时需要做一些技巧性的优化,否者性能会很差,而且Listview扩展性不太好,所以我们可以使用Android提供的更强大的滚动控件,RecyclerView,来实现视频列表。本篇博客先分享本地视频列表的实现,下篇博客将分享如何实现网络视频列表。


RecyclerView实现本地视频列表

添加依赖

新建项目,然后和前面使用bottomnavigationbar一样,在项目app的build.gradle文件的dependencies {}闭包中添加相应的依赖:

compile 'com.android.support:recyclerview-v7:25.3.1'

不要添加到项目的build.gradle文件中了。我发现Android Studio新建项目后的视图是“Android”,开发时,我们一般使用“Project”,会让我们对整个工程的目录结构看得比较清晰。如下图所示:

点击下拉菜单,选择“Project”。


准备VideoItem和VideoItemAdapter

可以不那么准确的理解,视频列表就是一个数组,这个数组里面有很多元素,每个元素就是一个VideoItem类的实例。VideoItem类包含一个视频列表项的基本信息,比如视频截图,视频长度,视频名字等等,一个不完整的示例如下:

public class VideoItem {public String name;public String path;public Bitmap thumb;public String createdTime;public String duration;VideoItem(String strPath, String strName, String strCreatedTime,String strDuration,Bitmap thumb) {this.path = strPath;this.name = strName;this.createdTime = strCreatedTime;this.duration = strDuration;this.thumb = thumb;}...public String getName() {return name;}public Bitmap getThumb() {return thumb;}.....

上面的类很简单,包含视频的名字,路径等属性,我们提供一个带参数的构造方法,当然,我们还需要提供相关的Getter和Setter方法。在Android Studio中,使用Alt+Insert来自动生成一些方法,比如Getter和Setter,Override方法等等。具体应用时,还需要考虑一些具体的情况,增加一些属性和其他的一些必要的方法。

现在我们已经有了“数组元素”,下一步就是把这些元素添加到一个List中,有了这个List,下一步就是如何展示这个List。但是,没法直接在RecyclerView和ListView这样的View中展示一个List,所以,我们还需要一个适配器,VideoItemAdapter。

到这里,我们需要理一下思路,VideoItem构成的VideoItemList,准备好了要展示数据,这是数据处理阶段;VideoItemAdapter负责把VideoItemList中的数据加载到RecyclerView中,这是View处理阶段。既然涉及到View,自然要有相关的布局。这里需要的是VideoItem的布局。这里需要一点类比的思想,VideoItemList包含很多数据子项,它们是一些VideoItem;而RecyclerView包含很多View子项,它们是一些ViewItem的view。怎么把数据展示到View中,这就是VideoItemAdapter的工作。

一个简单的videoItem的布局可以是:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:paddingTop="5dp"android:orientation="vertical"><ImageView
        android:id="@+id/video_thumb"android:layout_alignParentLeft="true"android:layout_marginLeft="15dp"android:layout_width="120dp"android:layout_height="90dp"android:scaleType="fitXY"android:src="@mipmap/ic_launcher"/><TextView
        android:id="@+id/video_title"android:layout_toRightOf="@+id/video_thumb"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="this is titile"android:textSize="16sp"android:layout_marginLeft="12dp"/><TextView
        android:id="@+id/video_date"android:layout_below="@+id/video_title"android:layout_toRightOf="@+id/video_thumb"android:layout_marginBottom="5dp"android:layout_marginLeft="12dp"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="video size"android:textSize="12sp"/><TextView
        android:id="@+id/video_duration"android:layout_below="@+id/video_title"android:layout_alignParentRight="true"android:layout_marginRight="15dp"android:layout_marginBottom="5dp"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="video time last"android:textSize="12sp"/></RelativeLayout>

它的显示效果为:


VideoItemAdapter继承自RecyclerView.Adapter,并且指定泛型为VideoItemAdapter.ViewHolder,ViewHolder是在VideoItemAdapter中定义的内部类。ViewHolder的构造函数需要传入一个View,这个View就是我们的RecyclerView包含的View子项的布局。VideoItemAdapter的完整代码如下:

package com.example.renkangchen.testlist;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
/*** Created by renkangchen on 17-6-2.*/
public class VideoItemAdapter extends RecyclerView.Adapter<VideoItemAdapter.ViewHolder>{private List<VideoItem> mVideoList;static class ViewHolder extends RecyclerView.ViewHolder{View videoListView;TextView textView_title;TextView textView_createTime;TextView textView_duration;ImageView thumb;public ViewHolder(View view){super(view);videoListView = view;textView_title = (TextView)view.findViewById(R.id.video_title);textView_createTime = (TextView)view.findViewById(R.id.video_date);textView_duration = (TextView)view.findViewById(R.id.video_duration);thumb = (ImageView)view.findViewById(R.id.video_thumb);}}public VideoItemAdapter(List<VideoItem> videoList){mVideoList = videoList;}@Overridepublic ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.video_item,parent,false);final ViewHolder holder = new ViewHolder(view);holder.videoListView.setOnClickListener(new View.OnClickListener(){@Overridepublic void onClick(View v) {int position = holder.getAdapterPosition();VideoItem videoItem = mVideoList.get(position);Toast.makeText(v.getContext(),"click on"+videoItem.getName(),Toast.LENGTH_SHORT).show();}});holder.thumb.setOnClickListener(new View.OnClickListener(){@Overridepublic void onClick(View v) {int position = holder.getAdapterPosition();VideoItem videoItem = mVideoList.get(position);Toast.makeText(v.getContext(),"click on image "+videoItem.getName(),Toast.LENGTH_SHORT).show();}});return holder;}@Overridepublic void onBindViewHolder(ViewHolder holder, int position) {VideoItem videoItem = mVideoList.get(position);holder.textView_title.setText(videoItem.getName());holder.textView_createTime.setText(videoItem.getCreatedTime());holder.textView_duration.setText(videoItem.getStrDuration());holder.thumb.setImageBitmap(videoItem.getThumb());}@Overridepublic int getItemCount() {return mVideoList.size();}}

看代码应该会更清楚些。我们用ViewHolder来得到一个videoItem的布局;VideoItemAdapter的构造方法用来传入要展示的数据; onCreateViewHolder方法用来创建ViewHolder的实例;onBindViewHolder方法会在一个RecyclerView的子项被滚动到屏幕范围内时调用,它用来给RecyclerView的子项赋值;getItemCount返回RecyclerView的子项数目。需要注意的是,我们必须 重写onCreateViewHolder,onBindViewHolder,getItemCount这三个方法。到这里,我们应该更明白这个VideoItemAdapter的作用了。

另外,我们在onCreateViewHolder的方法中添加了Recyclerview的点击监听。代码应该比较容易理解,不过需要注意的是,不同于ListView只能为Recyclerview子项整体添加,我们可以分别为Recyclerview子项的不同部分添加监听,这也是Recyclerview的优势所在。

用过ListView的同学应该有感觉,就是虽然两种方法的Adapter代码量差不多,但明显,Recyclerview的Adapter更容易理解些。而且不用自己去做一些优化工作,使用起来相对比较容易。


AsyncTask异步任务读取本地视频信息

之前的部分已经准备好了“碗筷”,现在就等“上菜”了。这里的“菜”便是我们从手机存储空间(这里不使用内存这样的概念是为了防止混淆)中读取到的视频信息。之前的博客中分享过,为了避免ANR,像读取存储空间,访问网络资源这样比较耗时的操作,都不能放主线程中,而应该开辟子线程去完成。但是多线程的使用并不是一件轻松的事情,因此,Android提供了AsyncTask类来方便我们更加轻松地完成在子线程中进行UI的更新工作。

AsyncTask类包含doInBackground,onProgressUpdate,onPostExecute等几个方法,使用时,我们把需要异步执行的任务放在doInBackground中,onProgressUpdate在异步任务更新时调用,用来动态更新UI,onPostExecute在异步任务结束时调用。AsyncTask类虽然已经帮我们简化了很多的工作,但是要用好AsyncTask也不是一件容易,有特别多需要注意的地方,有兴趣的同学可以自己搜索一下相关资料,或者看一下参考链接中的资料。

这里主要分享一下如何在doInBackground中扫描本地的视频信息。

private class VideoUpdateTask  extends AsyncTask<Void, VideoItem, Void> {List<VideoItem> mDataList = new ArrayList<VideoItem>();....}

首先我们创建一个VideoUpdateTask,它继承自AsyncTask,继承时,我们可以指定三个泛型参数,

参数 含义
Params 执行AsyncTask需要传入的参数,用于后台任务
Progress 后台任务执行过程中,使用这里指定的类型作为进度的单位,用于在界面上显示进度
Result 任务执行完毕时,使用这里指定的类型作为结果返回类型

于是我们可以这样定义

private class VideoUpdateTask  extends AsyncTask<Void, VideoItem, Void> 

Params参数指定为Void,表示我们不需要向后台任务传入什么参数;Progress参数指定为VideoItem,表示更新的单位为一个VideoItem;Result参数指定为Void,表示后台任务结束后,我们不需要返回什么结果。

需要注意的是,声明VideoUpdateTask时,需要使用

private AsyncTask<Void, VideoItem, Void> mVideoUpdateTask;

而不是

private AsyncTask mVideoUpdateTask;

或者直接声明成

private VideoUpdateTask mVideoUpdateTask;

这涉及到多态的一些知识,这里不作展开。
在 doInBackground(Void… params)的参数为Void类型的可变参数,如果你指定的Params为String,那么就应该写成 doInBackground(String… params)。

 @Overrideprotected Void doInBackground(Void... params) {Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;String[] searchKey = new String[] {MediaStore.Video.Media.TITLE,MediaStore.Images.Media.DATA,MediaStore.Images.Media.DATE_ADDED,MediaStore.Video.Media.DURATION};String where = null;//scan all video in the media storeString [] keywords = null;String sortOrder = MediaStore.Video.Media.DEFAULT_SORT_ORDER;ContentResolver resolver = getContentResolver();Cursor cursor = resolver.query(uri, searchKey, where, keywords, sortOrder);if(cursor != null){while(cursor.moveToNext() && ! isCancelled()){String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));String name = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.TITLE));String createdTime = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED));Long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));int duration2second = (int)(duration/1000);Bitmap thumb = ThumbnailUtils.createVideoThumbnail(path, MediaStore.Images.Thumbnails.MINI_KIND);VideoItem data = new VideoItem(path, name, createdTime,duration2second,thumb);Log.d(TAG, "doInBackground: video item ==== "+data.getName());publishProgress(data);mDataList.add(data);}cursor.close();}return null;}

这里的“扫描”其实准确地讲是从系统提供的媒体库中读取视频相关的数据,媒体库为我们提供了相应的内容提供器作为访问接口。内容提供器作为Android四大组件(内容提供器,活动,服务,广播接收器)之一,无疑是十分重要的知识,通过这个例子我们便可以了解内容提供器的用法,不过暂时我们不需要写自己的内容提供器,而是先试着用其他应用提供的内容提供器。要访问内容提供器的内容,就一定要使用ContextResolver:

  ContentResolver resolver = getContentResolver();

通过getContentResolver()来获取ContentResolver的一个实例。ContentResolver为我们提供了一系列的方法来进行增删查改操作(CRUD:Create,Retrieve,Update,Delete)。这里我们需要的方法为query,即查询系统媒体库中的视频信息。

 Cursor cursor = resolver.query(uri, searchKey, where, keywords, sortOrder);

query需要的几个参数为:

参数 含义
uri 指定查询的表名
projection 指定查询的列名
selection 指定where的约束条件
selectionArgs 指定为where中的占位符提供具体的值
orderBy 指定查询结果的排序方式
Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
String[] searchKey = new String[] {MediaStore.Video.Media.TITLE,MediaStore.Images.Media.DATA,MediaStore.Images.Media.DATE_ADDED,MediaStore.Video.Media.DURATION};
String where = null;//scan all video in the media store
String [] keywords = null;
String sortOrder = MediaStore.Video.Media.DEFAULT_SORT_ORDER;

uri我们直接使用MediaStore.Video.Media.EXTERNAL_CONTENT_URI,其中EXTERNAL_CONTENT_URI常量是一个Uri.parse()解析后的结果,所以直接使用就可以了;searchKey 中我们指定需要查询的列是TITLE,DATA,DATE_ADDED等几列;where条件指定为null,表示查询媒体库所有的视频,当然也可以指定查询特定路劲下的视频,可以用这样的方式

String where = MediaStore.Video.Media.DATA + " like \"%"+getString(R.string.search_path)+"%\"";

来查询特定路径下的视频;keywords同样设为null;sortOrder使用默认排序方式就好了。

查询的结果返回一个Cursor类型对象,Cursor翻译为光标或者是游标,在Android中,它是每行的集合。得到了返回的cursor,我们就可以把视频信息从cursor中逐个读取出来了

 if(cursor != null) {while(cursor.moveToNext() && ! isCancelled()){String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));String name = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.TITLE));String createdTime = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED));Long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));int duration2second = (int)(duration/1000);Bitmap thumb = ThumbnailUtils.createVideoThumbnail(path, MediaStore.Images.Thumbnails.MINI_KIND);VideoItem data = new VideoItem(path, name, createdTime,duration2second,thumb);Log.d(TAG, "doInBackground: video item ==== "+data.getName());publishProgress(data);mDataList.add(data);}cursor.close();
}

读取的方法就是通过移动moveToNext()游标的位置来遍历返回的Cursor对象中的所有行,然后在取出每一行中对应列的数据。需要注意最后的close操作,不要忘记了这个。有过关系数据库使用经历的同学会比较容易理解上面这些操作。注意到每次读取完一行数据后我们都使用了

publishProgress(data);
//publishProgress源码@WorkerThreadprotected final void publishProgress(Progress... values) {if (!isCancelled()) {getHandler().obtainMessage(MESSAGE_POST_PROGRESS,new AsyncTaskResult<Progress>(this, values)).sendToTarget();}}

publishProgress方法用来发送UI更新消息,它被调用后,onProgressUpdate很快会被调用,不可以在 doInBackground中调用onProgressUpdate方法,涉及到UI更新的操作都不可放在 doInBackground。onProgressUpdate可以是:

 @Overrideprotected void onProgressUpdate(VideoItem... values) {VideoItem data = values[0];mVideoList.add(data);mVideoItemAdapter.notifyDataSetChanged();}

VideoItem 就是我们声明AsyncTask时,指定的三个泛型

在MainAcivity中使用RecyclerView

要在在MainAcivity中使用RecyclerView,当然先要在MainAcivity的布局中添加这个view:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.example.renkangchen.testlist.MainActivity"><android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"android:layout_width="match_parent"android:layout_height="match_parent"></android.support.v7.widget.RecyclerView></FrameLayout>

RecyclerView的使用没有太大的差别

private LinearLayoutManager mLinearLayoutManager;

在onCreate中

mRecyclerView = (RecyclerView)findViewById(R.id.recyclerView);

另外mRecyclerView还需要用setLayoutManager方法来设置布局管理器(这个是必需的,比如你想横着显示列表,而不是竖着,比如你想要瀑布效果,都可以通过指定布局管理器来实现);用setAdapter方法来设置Adapter(这个也是必需的);用setItemAnimator来设置添加和移除的动画(这个是非必需的);用addItemDecoration来设置分割线的(这个也是非必需的,可以定制自己的分割线样式,而不是像Listview那样只能用默认的)。之前说过RecyclerView十分强大,意味着它是更自由的,所以和Listview比起来它似乎是更复杂了些,但这种“复杂”也让我们可以实现更多的定制的效果。

这里我们就简单些,只使用两个必要的方法,把RecyclerView的布局管理器指定为LinearLayoutManager,Adapter当然就是我们之前写的VideoItemAdapter了。

然后用异步任务VideoUpdateTask来”扫描“本地媒体库信息,并进行UI更新。需要注意的是,读取媒体库是几组敏感操作中的一个,所以必须要要进行权限申请,6.0以后需要动态申请权限,而不是直接在AndroidManifest中申请就可以了。

 if(Build.VERSION.SDK_INT >= 23) {if ((ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) !=PackageManager.PERMISSION_GRANTED) && (ContextCompat.checkSelfPermission(MainActivity.this,         Manifest.permission.READ_EXTERNAL_STORAGE) !=PackageManager.PERMISSION_GRANTED)) {//get the permissionActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},1);}//if without permissionelse{updateVideoList();}}//if higher sdk 23else{updateVideoList();}
...@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);switch (requestCode){case 1:if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){updateVideoList();}else{Toast.makeText(this,"You denied the Permission",Toast.LENGTH_SHORT).show();}break;default:break;}}

首先做个判断,6.0以后才需要动态申请,否者直接用AndroidManifest中获得的权限即可。这里申请了两个权限WRITE_EXTERNAL_STORAGE和READ_EXTERNAL_STORAGE,虽然只会用到读权限但读写往往在一起的,还是一起申请了吧。但动态权限机制引入的目的就是为了权限被滥用,所以规范来讲还是应该做到用什么权限就申请什么权限,而不是一来就要申请一大堆权限。onRequestPermissionsResult返回申请的结果,申请成功的话,我们就调用updateVideoList()这个方法:

  public void updateVideoList(){mVideoList.clear();mVideoUpdateTask = new VideoUpdateTask();mVideoUpdateTask.execute();}

它先清除mVideoList中的数据,然后新建一个VideoUpdateTask,并使用execute()方法来执行这个异步任务。

如果用户拒绝了权限申请,我们需要进行相应的操作反馈。如果是6.0以前的系统,那就直接调用updateVideoList()就好了。


结果

build工程,然后运行:

提示请求权限,这里我们选择同意。

可以看到,成功地读取了本地的视频文件(这里本地只有一个视频,为了展示列表的效果,就复制了几次,几个视频只是名字不同)。

点击其中一个视频列表项,触发点击事件,

点击其中一个视频列表项的视频缩略图,,触发另外一个点击事件。

总结一下,本篇博文涉及的内容较多,自己也感觉不论ListView还是RecyclerView都是Android众多控件中最常用也是最复杂难用的。时间比较紧张,写得很粗糙,疏漏错误之处,还请大家指正。下面是自己做此部分时阅读的一些参考资料。


Reference

手把手教你做视频播放器(三)-展示视频列表
第4章 展示视频列表
Android RecyclerView 使用完全解析 体验艺术般的控件
深入理解AsyncTask的工作原理
解读ContentResolver和ContentProvider
Android 中关于 【Cursor】 类的介绍
Android四大基本组件介绍与生命周期
Requesting Permissions(自备梯子)
Java多态性理解
Java Object类


参考源码

因为写博客时为了方便说明,会去掉一部分代码,所以建议下载完整的工程源码:
链接: https://pan.baidu.com/s/1boUg5P1 密码: h4rf

Android VR Player(全景视频播放器) [6]:视频列表的实现-本地视频相关推荐

  1. Android Video Player. 安卓视频播放器,封装 MediaPlayer、ExoPlayer、IjkPlayer。模仿抖音,悬浮播放,广告播放,列表播放,弹幕

    DKVideoPlayer 项目地址:dueeeke/DKVideoPlayer 简介: Android Video Player. 安卓视频播放器,封装 MediaPlayer.ExoPlayer. ...

  2. html怎样手机播放本地视频播放器,手机优酷怎么播放本地视频 本地文件导入方法...

    优酷下载的视频一般都会是KUX格式,只能使用优酷进行播放,比较霸道,那么反过来优酷是否可以播放本地视频呢?自然是可以的,下面就跟小编了解下吧. 方法一:找到视频选择打开方式 首先在智能手机的文件管理中 ...

  3. Android 视频播放器 VideoView 的使用,播放本地视频 和 网络 视频

    1.布局文件 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:and ...

  4. android vr播放器 开发,Android应用开发之Android VR Player(全景视频播放器)- ExoPlayer播放器MPEG-DASH视频播放...

    本文将带你了解Android应用开发之Android VR Player(全景视频播放器)- ExoPlayer播放器MPEG-DASH视频播放,希望本文对大家学Android有所帮助. Androi ...

  5. Android VR Player(全景视频播放器) [7]:视频列表的实现-网络视频

    Android VR Player(全景视频播放器) [7]:视频列表的实现-网络视频 前期准备 在之前的博文,Android VR Player(全景视频播放器) [6]:视频列表的实现-本地视频 ...

  6. android 全景播放器,Android VR Player(全景視頻播放器) [5]:簡單的歡迎界面

    Android VR Player(全景視頻播放器) [5]:簡單的歡迎界面 歡迎界面 在繼續下一部分,即視頻列表實現的介紹前,分享一下簡單的歡迎界面的實現.一來是可以整合一下前面說的側滑菜單和底部導 ...

  7. Android 全景视频播放器(VR视频播放器探索二)

        上次随便写着玩的  http://blog.csdn.net/ai_yong_jie/article/details/51159367   Android 全景视频播放器(VR视频播放器探索一 ...

  8. 基于Google的Android平台上GVR 3D全景视频播放器(支持本地文件和视频流传输)

    基于GVR(Google VR)安卓平台下的 3D全景视频播放器 Google GVR GVR简介 示例应用 源码实现 GVR关键的api调用 Gradle配置 效果图 布局 m3u8和hls协议(自 ...

  9. android 播放视频链接,如何通过Android视频播放器中的直接链接播放MP4视频?

    我正在制作一个 Android应用程序,我需要通过直接下载链接在Android默认本机视频播放器中播放mp4视频. 要打开Android视频播放器,我使用以下代码 Intent intent = ne ...

最新文章

  1. 使用 Application Developer V7 来创建和部署 JSR 168 协作 portlet
  2. 少糖的理由+1,新研究表明:高糖环境不利于肌肉修复和维持
  3. 超详细Pycharm部署项目视频教程
  4. 设计移动App的十大技巧
  5. 高并发负载均衡(二):LVS 的 DR,TUN,NAT 网络模型推导
  6. mysql5.6优化建议
  7. python3 robotframework+pycharm框架搭建
  8. python字符串类库_Python开发以太坊的类库Web3.py V4的新功能
  9. Mac mysql 运行sql文件中文乱码的问题
  10. jQuery源码解读一
  11. CCC认证有没有2019年新的具体的收费标准
  12. 公众号和订阅号的区别
  13. ROS节点运行管理launch文件
  14. 五大主流浏览器和内核
  15. 基于HTML+CSS制作静态页面【剪纸文化15页】传统文化设计题材 dreamweaver制作静态html网页设计作业作品...
  16. Lambertian 反射(也叫理想散射)
  17. html页边距为负值,css中的padding属性可以为负值吗?css中padding属性的详解
  18. 熊孩子乱敲键盘攻破linux桌面,“熊孩子”乱敲键盘就攻破了 Linux 桌面,大神:17 年前我就警告过你们...
  19. Unity发布WebGL运行问题
  20. 计算机用户名更改不了,分享简单几步解决win10电脑用户名改不了的问题

热门文章

  1. 情感分析论文阅读之《Aspect Level Sentiment Classification with Deep Memory Network》
  2. PDF怎么转换成PPT?几个步骤轻松转换
  3. 微软账户官方服务器名称是什么,如何为报表服务器注册服务主体名称 (SPN)
  4. cubieboard2使用ov7670模块
  5. [NSSCTF 2022 Spring Recruit] Crypto wp
  6. cpu_freq之切换governor.
  7. 在cad中按范围提取osgb和obj
  8. 隐藏任务栏后任务栏出不来怎么办?任务栏快捷键
  9. 关于约瑟夫问题(尾指针尾插法)
  10. 用Arduino测试ADXL335加速度计如何工作?