Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Android端实现1对1音视频实时通话

Android端实现1对1音视频实时通话

作者头像
音视频_李超
发布于 2020-04-02 13:33:32
发布于 2020-04-02 13:33:32
2.6K00
代码可运行
举报
运行总次数:0
代码可运行

前言

在学习 WebRTC 的过程中,学习的一个基本步骤是先通过 JS 学习 WebRTC的整体流程,在熟悉了整体流程之后,再学习其它端如何使用 WebRTC 进行互联互通。

本文将讲解 Android 端是如何使用WebRTC的,至于 P2P 穿越、STUN/TURN/ICE、RTP/RTCP协议、DTLS等内容不做讲解。

对这方面有兴趣的同学可以看我的视频课「 WebRTC实时互动直播技术入门与实战

申请权限

我们要使用 WebRTC 进行音视频互动时需要申请访问硬件的权限,至少要申请以下三种权限:

  • Camera 权限
  • Record Audio 权限
  • Intenet 权限

在Android中,申请权限分为静态权限申请和动态权限申请,这对于做 Android 开发的同学来说已经是习以为常的事情了。下面我们就看一下具体如何申请权限:

静态权限申请

在 Android 项目中的 AndroidManifest.xml 中增加以下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...

<uses-feature android:name="android.hardware.camera" />
<uses-feature
    android:glEsVersion="0x00020000"
    android:required="true" />

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTENET" />

...

动态权限申请

随着 Android 的发展,对安全性要求越来越高。除了申请静态权限之外,还需要动态申请权限。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void requestPermissions(String[] permissions, intrequestCode);

实际上,对于权限这块的处理真正做细了要写不少代码,好在 Android 官方给我们又提供了一个非常好用的库 EasyPermissions , 有了这个库我们可以少写不少代码。使用 EasyPermissions 非常简单,在MainActivity中添加代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...

protected void onCreate ( Bundle savedInstanceState ) {
    ...

    String[] perms = {
                Manifest.permission.CAMERA,
                Manifest.permission.RECORD_AUDIO
    };

    if (!EasyPermissions.hasPermissions(this, perms)) {
        EasyPermissions.requestPermissions(this, 
                                           "Need permissions for camera & microphone", 
                                            0, 
                                            perms);
    }
}

@Override
public void onRequestPermissionsResult(int requestCode,
                                       String[] permissions, 
                                       int[] grantResults) {

    super.onRequestPermissionsResult(requestCode, 
                                     permissions, 
                                     grantResults);

    EasyPermissions.onRequestPermissionsResult(requestCode,
                                               permissions, 
                                               grantResults, 
                                               this);
}

...

通过添加以上代码,就将权限申请好了,是不是非常简单?权限申请好了,我们开始做第二步,看在 Android 下如何引入 WebRTC 库。

引入库

在我们这个例子中要引入两个比较重要的库,第一个当然就是 WebRTC 库了,第二个是 socket.io库,用它来与信令服务器互联。

首先我们看一下如何引入 WebRTC 库(我这里使用的是最新 Android Studio 3.3.2)。在 Module 级别的 build.gradle 文件中增加以下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
dependencies {
    ...
    implementation 'org.webrtc:google-webrtc:1.0.+'
    ...
}

是不是非常简单?

接下来要引入 socket.io 库,用它来与我们之前用 Nodejs 搭建的信令服务器进行对接。再加上前面用到的EasyPermissions库,所以真正的代码应写成下面的样子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
dependencies {
    ...
    implementation 'io.socket:socket.io-client:1.0.0'
    implementation 'org.webrtc:google-webrtc:1.0.+'
    implementation 'pub.devrel:easypermissions:1.1.3'
}

通过上面的方式我们就将需要引入的库全部引入进来了。下面就可以开始真的 WebRTC 之旅了。

万物的开始

我们都知道万物有个起源,我们在开发 WebRTC 程序时也不例外,WebRTC程序的起源就是PeerConnectionFactory。这也是与使用 JS 开发 WebRTC 程序最大的不同点之一,因为在 JS 中不需要使用 PeerConnectionFactory 来创建 PeerConnection 对象。

而在 Android/iOS 开发中,我们使用的 WebRTC 中的大部分对象基本上都是通过 PeerConnectionFactory 创建出来的。下面这张图就清楚的表达了 PeerConnectionFactory 在 WebRTC 中的地位。

通过该图我们可以知道,WebRTC中的核心对象 PeerConnection、LocalMediaStream、LocalVideoTrack、LocalAudioTrack都是通过 WebRTC 创建出来的。

PeerConnectionFactory的初始化与构造

在 WebRTC 中使用了大量的设计模式,对于 PeerConnectionFactory 也是如此。它本身就是工厂模式,而这个构造 PeerConnection 等核心对象的工厂又是通过 builder 模式构建出来的。

下面我们就来看看如何构造 PeerConectionFactory。在我们构造 PeerConnectionFactory 之前,首先要对其进行初始化,其代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PeerConnectionFactory.initialize(...);

初始化之后,就可以通过 builder 模式来构造 PeerConnecitonFactory 对象了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...

PeerConnectionFactory.Builder builder =         
                PeerConnectionFactory.builder()
                    .setVideoEncoderFactory(encoderFactory)
                    .setVideoDecoderFactory(decoderFactory);

 ...

 return builder.createPeerConnectionFactory();

通过上面的代码,大家也就能够理解为什么 WebRTC 要使用 buider 模式来构造 PeerConnectionFactory 了吧?主要是方便调整建造 PeerConnectionFactory的组件,如编码器、解码器等。

从另外一个角度我们也可以了解到,要更换WebRTC引警的编解码器该从哪里设置了哈!

音视频数据源

有了PeerConnectionFactory对象,我们就可以创建数据源了。实际上,数据源是 WebRTC 对音视频数据的一种抽象,表式数据可以从这里获取。

使用过 JS WebRTC API的同学都非常清楚,在 JS中 VideoTrack 和 AudioTrack 就是数据源。而在 Android 开发中我们可以知道 Video/AudioTrack 就是 Video/AudioSouce的封装,可以认为他们是等同的。

创建数据源的方式如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
VideoSource videoSource = 
                    mPeerConnectionFactory.createVideoSource(false);
mVideoTrack = mPeerConnectionFactory.createVideoTrack(
                                                    VIDEO_TRACK_ID, 
                                                    videoSource);

...

AudioSource audioSource = 
                    mPeerConnectionFactory.createAudioSource(new MediaConstraints());
mAudioTrack = mPeerConnectionFactory.createAudioTrack(
                                                    AUDIO_TRACK_ID, 
                                                    audioSource);

...                                                 

数据源只是对数据的一种抽象,它是从哪里获取的数据呢?对于音频来说,在创建 AudioSource时,就开始从音频设备捕获数据了。对于视频来说我们可以指定采集视频数据的设备,然后使用观察者模式从指定设备中获取数据。

接下来我们就来看一下如何指定视频设备。

视频采集

在 Android 系统下有两种 Camera,一种称为 Camera1, 是一种比较老的采集视频数据的方式,别一种称为 Camera2, 是一种新的采集视频的方法。它们之间的最大区别是 Camera1使用同步方式调用API,Camera2使用异步方式,所以Camera2更高效。

我们看一下 WebRTC 是如何指定具体的 Camera 的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private VideoCapturer createVideoCapturer() {
        if (Camera2Enumerator.isSupported(this)) {
            return createCameraCapturer(new Camera2Enumerator(this));
        } else {
            return createCameraCapturer(new Camera1Enumerator(true));
        }
}

private VideoCapturer createCameraCapturer(CameraEnumerator enumerator) {
        final String[] deviceNames = enumerator.getDeviceNames();

        // First, try to find front facing camera
        Log.d(TAG, "Looking for front facing cameras.");
        for (String deviceName : deviceNames) {
            if (enumerator.isFrontFacing(deviceName)) {
                Logging.d(TAG, "Creating front facing camera capturer.");
                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
                if (videoCapturer != null) {
                    return videoCapturer;
                }
            }
        }

        // Front facing camera not found, try something else
        Log.d(TAG, "Looking for other cameras.");
        for (String deviceName : deviceNames) {
            if (!enumerator.isFrontFacing(deviceName)) {
                Logging.d(TAG, "Creating other camera capturer.");
                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
                if (videoCapturer != null) {
                    return videoCapturer;
                }
            }
        }

        return null;
}

上面代码的逻辑也比较简单:

  • 首先看 Android 设备是否支持 Camera2.
  • 如果支持就使用 Camera2, 如果不支持就使用 Camera1.
  • 在获到到具体的设备后,再看其是否有前置摄像头,如果有就使用
  • 如果没有有效的前置摄像头,则选一个非前置摄像头。

通过上面的方法就可以拿到使用的摄像头了,然后将摄像头与视频源连接起来,这样从摄像头获取的数据就源源不断的送到 VideoTrack 里了。

下面我们来看看 VideoCapture 是如何与 VideoSource 关联到一起的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...

mSurfaceTextureHelper = 
            SurfaceTextureHelper.create("CaptureThread",
                                        mRootEglBase.getEglBaseContext());

mVideoCapturer.initialize(mSurfaceTextureHelper,
                          getApplicationContext(), 
                          videoSource.getCapturerObserver());

...

mVideoTrack.setEnabled(true);
...

上面的代码中,在初始化 VideoCaptuer 的时候,可以过观察者模式将 VideoCapture 与 VideoSource 联接到了一起。因为 VideoTrack 是 VideoSouce 的一层封装,所以此时我们开启 VideoTrack 后就可以拿到视频数据了。

当然,最后还要调用一下 VideoCaptuer 对象的 startCapture 方法真正的打开摄像头,这样 Camera 才会真正的开始工作哈,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Override
protected void onResume() {
    super.onResume();
    mVideoCapturer.startCapture(VIDEO_RESOLUTION_WIDTH, 
                                VIDEO_RESOLUTION_HEIGHT, 
                                VIDEO_FPS);
}

拿到了视频数据后,我们如何将它展示出来呢?

渲染视频

在 Android 下 WebRTC 使用OpenGL ES 进行视频渲染,用于展示视频的控件是 WebRTC 对 Android 系统控件 SurfaceView 的封装。

WebRTC 封装后的 SurfaceView 类为 org.webrtc.SurfaceViewRenderer。在界面定义中应该定义两个SurfaceViewRenderer,一个用于显示本地视频,另一个用于显示远端视频。

其定义如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...

<org.webrtc.SurfaceViewRenderer
        android:id="@+id/LocalSurfaceView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

<org.webrtc.SurfaceViewRenderer
    android:id="@+id/RemoteSurfaceView"
    android:layout_width="120dp"
    android:layout_height="160dp"
    android:layout_gravity="top|end"
    android:layout_margin="16dp"/>

...

通过上面的代码我们就将显示视频的 View 定义好了。光定义好这两个View 还不够,还要对它做进一步的设置:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...

mLocalSurfaceView.init(mRootEglBase.getEglBaseContext(), null);
mLocalSurfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
mLocalSurfaceView.setMirror(true);
mLocalSurfaceView.setEnableHardwareScaler(false /* enabled */);

...

其含义是:

  • 使用 OpenGL ES 的上下文初始化 View。
  • 设置图像的拉伸比例。
  • 设置图像显示时反转,不然视频显示的内容与实际内容正好相反。
  • 是否打开便件进行拉伸。

通过上面的设置,我们的 view 就设置好了,对于远端的 Veiw 与本地 View 的设置是一样的,我这里就不再赘述了。

接下来将从摄像头采集的数据设置到该view里就可以显示了。设置非常的简单,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
mVideoTrack.addSink(mLocalSurfaceView);
...

对于远端来说与本地视频的渲染显示是类似的,只不过数据源是从网络获取的。

通过以上讲解,大家应该对 WebRTC 如何采集数据、如何渲染数据有了基本的认识。下面我们再看来下远端的数据是如何来的。

创建 PeerConnection

要想从远端获取数据,我们就必须创建 PeerConnection 对象。该对象的用处就是与远端建立联接,并最终为双方通讯提供网络通道。

我们来看下如何创建 PeerConnecion 对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
PeerConnection.RTCConfiguration rtcConfig = 
                new PeerConnection.RTCConfiguration(iceServers);
...
PeerConnection connection =
                mPeerConnectionFactory.createPeerConnection(rtcConfig,
                                                            mPeerConnectionObserver);

...
connection.addTrack(mVideoTrack, mediaStreamLabels);
connection.addTrack(mAudioTrack, mediaStreamLabels);
...

PeerConnection 对象的创建还是要使我们之前讲过的 PeerConnectionFactory 来创建。WebRTC 在建立连接时使用 ICE 架构,一些参数需要在创建 PeerConnection 时设置进去。

另外,当 PeerConnection 对象创建好后,我们应该将本地的音视频轨添加进去,这样 WebRTC 才能帮我们生成包含相应媒体信息的 SDP,以便于后面做媒体能力协商使用。

通过上面的方式,我们就将 PeerConnection 对象创建好了。与 JS 中的 PeerConnection 对象一样,当其创建好之后,可以监听一些我们感兴趣有事件了,如收到 Candidate 事件时,我们要与对方进行交换。

PeerConnection 事件的监听与 JS 还是有一点差别的。在 JS 中,监听 PeerConnection的相关事件非常直接,直接实现peerconnection.onXXX就好了。而 Android 中的方式与 JS 略有区别,它是通过观察者模式来监听事件的。大家这点一定要注意!

双方都创建好 PeerConnecton 对象后,就会进行媒体协商,协商完成后,数据在底层就开始传输了。

信令驱动

在整个 WebRTC 双方交互的过程中,其业务逻辑的核心是信令, 所有的模块都是通过信令串联起来的。

以 PeerConnection 对象的创建为例,该在什么时候创建 PeerConnection 对象呢?最好的时机当然是在用户加入房间之后了 。

下面我们就来看一下,对于两人通讯的情况,信令该如何设计。在我们这个例子中,可以将信令分成两大类。第一类为客户端命令;第二类为服务端命令;

客户端命令有:

  • join: 用户加入房间
  • leave: 用户离开房间
  • message: 端到端命令(offer、answer、candidate)

服务端命令:

  • joined: 用户已加入
  • leaved: 用户已离开
  • other_joined:其它用户已加入
  • bye: 其它用户已离开
  • full: 房间已满

通过以上几条信令就可以实现一对一实时互动的要求,是不是非常的简单?

在本例子中我们仍然是通过socket.io与之前搭建的信令服备器互联的。由于 socket.io 是跨平台的,所以无论是在 js 中,还是在 Android 中,我们都可以使用其客户端与服务器相联,非常的方便。

下面再来看一下,收到不同信令后,客户端的状态变化:

客户端一开始的时候处于 Init/Leave 状态。当发送 join 消息,并收到服务端的 joined 后,其状态变为 joined。

此时,如果第二个用户加入到房间,则客户端的状态变为了 joined_conn, 也就是说此时双方可以进行实时互动了。

如果此时,该用户离开,则其状态就变成了 初始化状态。其它 case 大家可以根据上面的图自行理解了。

小结

本文首先介绍了在 Android 中使用 WebRTC 要需申请的权限,以及如何引入 WebRTC 库。然后从如何采集音视频数据、如何渲染、如何与对方建立连接等几个方面向大家详细介绍了如何在 Android 系统下开发一套 1对1的直播系统。

本文介绍的知识与我之前所写的通过 《Nodejs 搭建 WebRTC 信令服务器》完整的构成了一套 1对1直播系统。希望通过本文的学习,同学们可以快速的撑握 WebRTC 的使用,并根据自己的需要构建自己的直播系统。

谢谢!

参考

WebRTC实时互动直播技术入门与实战

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【AI 大模型】使用 AI 大模型 编程 ② ( CodeGeeX 工具 | CodeGeeX 功能 | VSCode 安装使用 CodeGeeX | Tabby 工具 | Tabby 部署与使用 )
CodeGeeX 可以作为 GitHub Copilot 的平替 , 二者功能基本一致 ;
韩曙亮
2024/08/09
5190
【AI 大模型】使用 AI 大模型 编程 ② ( CodeGeeX 工具 | CodeGeeX 功能 | VSCode 安装使用 CodeGeeX | Tabby 工具 | Tabby 部署与使用 )
【Web APIs】JavaScript 操作元素 ⑥ ( 关闭对话框案例 | display 属性简介 | 页面标签结构和样式 | 盒子模型细节 | 绝对布局要点 - 设置负值即可超出父容器模型 )
在 【Web APIs】JavaScript 操作元素 ① ( 修改元素内容 | innerText 属性修改元素文本内容 | innerHTML 属性修改元素 HTML 内容 ) 博客中介绍了 使用
韩曙亮
2024/08/09
2020
【Web APIs】JavaScript 操作元素 ⑥ ( 关闭对话框案例 | display 属性简介 | 页面标签结构和样式 | 盒子模型细节 | 绝对布局要点 - 设置负值即可超出父容器模型 )
【Web APIs】JavaScript 操作元素 ⑤ ( 修改元素样式属性 | 行内样式操作 - element.style | 类名样式操作 | 类列表样式操作 )
在 【Web APIs】JavaScript 操作元素 ① ( 修改元素内容 | innerText 属性修改元素文本内容 | innerHTML 属性修改元素 HTML 内容 ) 博客中介绍了 使用
韩曙亮
2024/08/09
2130
【Web APIs】JavaScript 操作元素 ⑤ ( 修改元素样式属性 | 行内样式操作 - element.style | 类名样式操作 | 类列表样式操作 )
【移动端网页布局】Flex 弹性布局案例 ② ( 顶部固定定位搜索栏 | 固定定位盒子居中对齐 | 二倍精灵图设置 | CSS3 中的垂直居中对齐 )
首先 , 设置固定定位 , 固定定位盒子始终显示在浏览器中指定的位置 , 与父容器或其它容器无关 ;
韩曙亮
2023/10/11
5200
【移动端网页布局】Flex 弹性布局案例 ② ( 顶部固定定位搜索栏 | 固定定位盒子居中对齐 | 二倍精灵图设置 | CSS3 中的垂直居中对齐 )
【如果你要学JS <16>】—— 表单元素的属性操作,密码显示隐藏的实现.
根据早中晚不同时间,进行一个判断,然后再进行一个事件的改变,近而可以从不同时间段获得不同的图片状态
像素人
2023/12/30
2860
【如果你要学JS <16>】—— 表单元素的属性操作,密码显示隐藏的实现.
前端之HTML和CSS
  HTML是 HyperText Mark-up Language 的首字母简写,意思是超文本标记语言,超文本指的是超链接,标记指的是标签,是一种用来制作网页的语言,这种语言由一个个的标签组成,用这种语言制作的文件保存的是一个文本文件,文件的扩展名为html或者htm。
汪凡
2019/03/01
4.4K0
前端之HTML和CSS
【移动端网页布局】Flex 弹性布局案例 ③ ( 横向导航栏 | 固定定位下面的布局设置 | 设置横向导航栏弹性布局 | 弹性布局主轴和侧轴设置 | 二倍精灵图 )
在上一篇博客中实现的搜索栏 , 使用 固定定位 设置该搜索栏位置 , 不管网页如何滚动 , 最上方始终显示该搜索栏 ;
韩曙亮
2023/10/11
6860
【移动端网页布局】Flex 弹性布局案例 ③ ( 横向导航栏 | 固定定位下面的布局设置 | 设置横向导航栏弹性布局 | 弹性布局主轴和侧轴设置 | 二倍精灵图 )
【移动端网页布局】流式布局案例 ⑦ ( 水平排列的图片链接 2 | 浮动设置 | 盒子模型类型设置 | 结构伪类选择器 )
使用 <div> 标签作为父盒子 , 其中容纳三个 链接 <a> 标签 , 每个链接标签中包含一个 <img> 标签 ;
韩曙亮
2023/05/04
2.4K0
【移动端网页布局】流式布局案例 ⑦ ( 水平排列的图片链接 2 | 浮动设置 | 盒子模型类型设置 | 结构伪类选择器 )
【移动端网页布局】流式布局案例 ⑥ ( 多排按钮导航栏 | 设置浮动及宽度 | 设置图片样式 | 设置文本 )
该导航栏的宽度自动充满整个屏幕 , 宽度为 100% , 高度也不需要设置 , 设置自适应即可 ;
韩曙亮
2023/05/04
3.5K0
【移动端网页布局】流式布局案例 ⑥ ( 多排按钮导航栏 | 设置浮动及宽度 | 设置图片样式 | 设置文本 )
使用 CSS 的仿 GitHub 登录页面
在线演示地址:https://haiyong.site/demo/github.html 码上掘金地址:https://code.juejin.cn/pen/7130522560411729934
海拥
2022/09/28
1.8K0
使用 CSS 的仿 GitHub 登录页面
【CSS】课程网站头部制作 ④ ( 搜索栏按钮测量 | 搜索栏按钮代码编写 | 代码示例 )
文章目录 一、搜索栏按钮测量 1、按钮测量 2、按钮切图 二、搜索栏按钮代码编写 1、HTML 标签结构 2、CSS 样式 3、展示效果 一、搜索栏按钮测量 ---- 1、按钮测量 右侧的按钮大小 , 50 x 40 像素 ; 按钮颜色值 #00a4ff ; 2、按钮切图 使用 切片工具 , 将 按钮图片 进行选择 , 然后进行切图 ; 选择 " 菜单栏 / 文件 / 导出 / 存储为 Web 所用格式 " , 选择导出的格式 , 以及 切好的图片如下 : 二、搜索栏按钮
韩曙亮
2023/04/03
2.4K0
【CSS】课程网站头部制作 ④ ( 搜索栏按钮测量 | 搜索栏按钮代码编写 | 代码示例 )
【移动端网页布局】流式布局案例 ⑤ ( 连续排列的链接图片 | 设置盒子圆角 | 超出部分隐藏 | 设置浮动布局 | 精确计算浮动元素宽度避免换行 | 设置图片宽度自适应 )
将布局中的 三个 链接图片 , 放置在 单独的 <div> 标签中 , 每个 <div> 标签中放置一个 <a> 链接标签 , 在 <a> 链接标签中包裹一个 <img> 图片 ;
韩曙亮
2023/05/04
3.7K0
【移动端网页布局】流式布局案例 ⑤ ( 连续排列的链接图片 | 设置盒子圆角 | 超出部分隐藏 | 设置浮动布局 | 精确计算浮动元素宽度避免换行 | 设置图片宽度自适应 )
【Web APIs】JavaScript 操作元素 ⑦ ( 多精灵图背景设置 | 核心要点 - 设置 backgroundPosition 属性 | 清除默认样式 | )
本案例中 , 就 使用了 <li> 元素 进行页面结构布局 , 布局时需要使用到 浮动 样式 , 计算换行时 , 每个像素宽高都要精确计算 ;
韩曙亮
2024/08/09
1640
【Web APIs】JavaScript 操作元素 ⑦ ( 多精灵图背景设置 | 核心要点 - 设置 backgroundPosition 属性 | 清除默认样式 | )
【CSS】课程网站头部制作 ③ ( 搜索栏表单测量 | 搜索栏表单代码编写 | 代码示例 )
导航栏文本 有 10 像素内边距 , 20 像素外边距 , 文本输入框 表单 , 距离 导航栏外边距有 65 像素 ;
韩曙亮
2023/04/03
2K0
【CSS】课程网站头部制作 ③ ( 搜索栏表单测量 | 搜索栏表单代码编写 | 代码示例 )
css属性及定位操作
font-family可以把多个字体名称作为一个“回退”系统来保存。如果浏览器不支持第一个字体,则会尝试下一个。浏览器会使用它可识别的第一个值。
全栈程序员站长
2022/07/21
2.5K0
css属性及定位操作
css笔记
从HTML被发明开始,样式就以各种形式存在。不同的浏览器结合它们各自的样式语言为用户提供页面效果的控制。最初的HTML只包含很少的显示属性。 随着HTML的成长,为了满足页面设计者的要求,HTML添加了很多显示功能。但是随着这些功能的增加,HTML变的越来越杂乱,而且HTML页面也越来越臃肿。于是CSS便生了。
用户6362579
2019/09/29
7.8K0
css笔记
【移动端网页布局】流式布局案例 ③ ( 实现搜索栏功能 | 伪元素选择器 | 子绝父相 | 外边距塌陷处理 | 二倍精灵图处理方案 )
在调试模式下 , 该父容器的尺寸为 390 x 44 像素 , 该父容器的高度是 44 像素 ;
韩曙亮
2023/05/03
2.1K0
【移动端网页布局】流式布局案例 ③ ( 实现搜索栏功能 | 伪元素选择器 | 子绝父相 | 外边距塌陷处理 | 二倍精灵图处理方案 )
【CSS】课程网站横版导航栏 ( 横版导航栏测量及样式 | 代码示例 )
该盒子是处于版心位置 , 先为其设置版心的样式 , 版心宽度 1200 像素 , 水平居中 , 先将版心的样式设置给盒子 ;
韩曙亮
2023/04/06
5.3K0
【CSS】课程网站横版导航栏 ( 横版导航栏测量及样式 | 代码示例 )
【原创】CSS中的盒子模型以及设置元素居中
元素的宽度:内容区宽度 + 左右内边距宽度 + 左右边框的宽度 + 左右外边距的宽度。
零点
2023/03/03
1K0
CSS入门?一篇就够了!
CSS通常称为CSS样式表或层叠样式表(级联样式表),主要用于设置HTML页面中的文本内容(字体、大小、对齐方式等)、图片的外形(宽高、边框样式、边距等)以及版面的布局等外观显示样式。
对话、
2022/02/22
5.3K0
CSS入门?一篇就够了!
推荐阅读
【AI 大模型】使用 AI 大模型 编程 ② ( CodeGeeX 工具 | CodeGeeX 功能 | VSCode 安装使用 CodeGeeX | Tabby 工具 | Tabby 部署与使用 )
5190
【Web APIs】JavaScript 操作元素 ⑥ ( 关闭对话框案例 | display 属性简介 | 页面标签结构和样式 | 盒子模型细节 | 绝对布局要点 - 设置负值即可超出父容器模型 )
2020
【Web APIs】JavaScript 操作元素 ⑤ ( 修改元素样式属性 | 行内样式操作 - element.style | 类名样式操作 | 类列表样式操作 )
2130
【移动端网页布局】Flex 弹性布局案例 ② ( 顶部固定定位搜索栏 | 固定定位盒子居中对齐 | 二倍精灵图设置 | CSS3 中的垂直居中对齐 )
5200
【如果你要学JS <16>】—— 表单元素的属性操作,密码显示隐藏的实现.
2860
前端之HTML和CSS
4.4K0
【移动端网页布局】Flex 弹性布局案例 ③ ( 横向导航栏 | 固定定位下面的布局设置 | 设置横向导航栏弹性布局 | 弹性布局主轴和侧轴设置 | 二倍精灵图 )
6860
【移动端网页布局】流式布局案例 ⑦ ( 水平排列的图片链接 2 | 浮动设置 | 盒子模型类型设置 | 结构伪类选择器 )
2.4K0
【移动端网页布局】流式布局案例 ⑥ ( 多排按钮导航栏 | 设置浮动及宽度 | 设置图片样式 | 设置文本 )
3.5K0
使用 CSS 的仿 GitHub 登录页面
1.8K0
【CSS】课程网站头部制作 ④ ( 搜索栏按钮测量 | 搜索栏按钮代码编写 | 代码示例 )
2.4K0
【移动端网页布局】流式布局案例 ⑤ ( 连续排列的链接图片 | 设置盒子圆角 | 超出部分隐藏 | 设置浮动布局 | 精确计算浮动元素宽度避免换行 | 设置图片宽度自适应 )
3.7K0
【Web APIs】JavaScript 操作元素 ⑦ ( 多精灵图背景设置 | 核心要点 - 设置 backgroundPosition 属性 | 清除默认样式 | )
1640
【CSS】课程网站头部制作 ③ ( 搜索栏表单测量 | 搜索栏表单代码编写 | 代码示例 )
2K0
css属性及定位操作
2.5K0
css笔记
7.8K0
【移动端网页布局】流式布局案例 ③ ( 实现搜索栏功能 | 伪元素选择器 | 子绝父相 | 外边距塌陷处理 | 二倍精灵图处理方案 )
2.1K0
【CSS】课程网站横版导航栏 ( 横版导航栏测量及样式 | 代码示例 )
5.3K0
【原创】CSS中的盒子模型以及设置元素居中
1K0
CSS入门?一篇就够了!
5.3K0
相关推荐
【AI 大模型】使用 AI 大模型 编程 ② ( CodeGeeX 工具 | CodeGeeX 功能 | VSCode 安装使用 CodeGeeX | Tabby 工具 | Tabby 部署与使用 )
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验