iOS视频开发之AVPlayer

AVPlayer这里我就不自己写了,转载一篇别人写的博客,十分好,大家可以看看:
原文地址:http://www.cnblogs.com/YanPengBlog/p/5280211.html

//MPMoviePlayerController足够强大,几乎不用写几行代码就能完成一个播放器,但是正是由于它的高度封装使得要自定义这个播放器变得很复杂,甚至是不可能完成。在IOS9之后,MPMoviePlayerController就被苹果弃用了(不过不影响正常使用),苹果推荐使用AVPlayerViewController,简而言之就是MPMoviePlayerController使用更简单,功能不如AVPlayer强大,而AVPlayer使用稍微麻烦点,不过功能更加强大。例如有些时候需要自定义播放器的样式,那么如果要使用MPMoviePlayerController就不合适了,如果要对视频有自由的控制则可以使用AVPlayer。AVPlayer存在于AVFoundation中,它更加接近于底层,所以灵活性也更强:

这里写图片描述

//AVPlayer本身并不能显示视频,而且它也不像MPMoviePlayerController有一个view属性。如果AVPlayer要显示必须创建一个播放器层AVPlayerLayer用于展示,播放器层继承于CALayer,有了AVPlayerLayer之添加到控制器视图的layer中即可。要使用AVPlayer首先了解一下几个常用的类:

//AVAsset:主要用于获取多媒体信息,是一个抽象类,不能直接使用。

//AVURLAsset:AVAsset的子类,可以根据一个URL路径创建一个包含媒体信息的AVURLAsset对象。

//AVPlayerItem:一个媒体资源管理对象,管理者视频的一些基本信息和状态,一个AVPlayerItem对应着一个视频资源

下面简单通过一个播放器来演示AVPlayer的使用,播放器的效果如下:

这里写图片描述

在这个自定义的播放器中实现了视频播放、暂停、进度展示和视频列表功能,下面将对这些功能一一介绍。

首先说一下视频的播放、暂停功能,这也是最基本的功能,AVPlayer对应着两个方法play、pause来实现。但是关键问题是如何判断当前视频是否在播放,在前面的内容中无论是音频播放器还是视频播放器都有对应的状态来判断,但是AVPlayer却没有这样的状态属性,通常情况下可以通过判断播放器的播放速度来获得播放状态。如果rate为0说明是停止状态,1是则是正常播放状态。

其次要展示播放进度就没有其他播放器那么简单了。在前面的播放器中通常是使用通知来获得播放器的状态,媒体加载状态等,但是无论是AVPlayer还是AVPlayerItem(AVPlayer有一个属性currentItem是AVPlayerItem类型,表示当前播放的视频对象)都无法获得这些信息。当然AVPlayerItem是有通知的,但是对于获得播放状态和加载状态有用的通知只有一个:播放完成通知AVPlayerItemDidPlayToEndTimeNotification。在播放视频时,特别是播放网络视频往往需要知道视频加载情况、缓冲情况、播放情况,这些信息可以通过KVO监控AVPlayerItem的status、loadedTimeRanges属性来获得。当AVPlayerItem的status属性为AVPlayerStatusReadyToPlay是说明正在播放,只有处于这个状态时才能获得视频时长等信息;当loadedTimeRanges的改变时(每缓冲一部分数据就会更新此属性)可以获得本次缓冲加载的视频范围(包含起始时间、本次加载时长),这样一来就可以实时获得缓冲情况。然后就是依靠AVPlayer的- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block方法获得播放进度,这个方法会在设定的时间间隔内定时更新播放进度,通过time参数通知客户端。相信有了这些视频信息播放进度就不成问题了,事实上通过这些信息就算是平时看到的其他播放器的缓冲进度显示以及拖动播放的功能也可以顺利的实现。

最后就是视频切换的功能,在前面介绍的所有播放器中每个播放器对象一次只能播放一个视频,如果要切换视频只能重新创建一个对象,但是AVPlayer却提供了- (void)replaceCurrentItemWithPlayerItem:(AVPlayerItem *)item方法用于在不同的视频之间切换(事实上在AVFoundation内部还有一个AVQueuePlayer专门处理播放列表切换,有兴趣的朋友可以自行研究,这里不再赘述)。

//
//  ViewController.m
//  AVPlayer
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

@interface ViewController ()

@property (nonatomic,strong) AVPlayer *player;//播放器对象

@property (weak, nonatomic) IBOutlet UIView *container; //播放器容器
@property (weak, nonatomic) IBOutlet UIButton *playOrPause; //播放/暂停按钮
@property (weak, nonatomic) IBOutlet UIProgressView *progress;//播放进度

@end

@implementation ViewController

#pragma mark - 控制器视图方法
- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupUI];
    [self.player play];
}

-(void)dealloc{
    [self removeObserverFromPlayerItem:self.player.currentItem];
    [self removeNotification];
}

#pragma mark - 私有方法
-(void)setupUI{
    //创建播放器层
    AVPlayerLayer *playerLayer=[AVPlayerLayer playerLayerWithPlayer:self.player];
    playerLayer.frame=self.container.frame;
    //playerLayer.videoGravity=AVLayerVideoGravityResizeAspect;//视频填充模式
    [self.container.layer addSublayer:playerLayer];
}

/**
 *  截取指定时间的视频缩略图
 *
 *  @param timeBySecond 时间点
 */

/**
 *  初始化播放器
 *
 *  @return 播放器对象
 */
-(AVPlayer *)player{
    if (!_player) {
        AVPlayerItem *playerItem=[self getPlayItem:0];
        _player=[AVPlayer playerWithPlayerItem:playerItem];
        [self addProgressObserver];
        [self addObserverToPlayerItem:playerItem];
    }
    return _player;
}

/**
 *  根据视频索引取得AVPlayerItem对象
 *
 *  @param videoIndex 视频顺序索引
 *
 *  @return AVPlayerItem对象
 */
-(AVPlayerItem *)getPlayItem:(int)videoIndex{
    NSString *urlStr=[NSString stringWithFormat:@"http://192.168.1.161/%i.mp4",videoIndex];
    urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url=[NSURL URLWithString:urlStr];
    AVPlayerItem *playerItem=[AVPlayerItem playerItemWithURL:url];
    return playerItem;
}
#pragma mark - 通知
/**
 *  添加播放器通知
 */
-(void)addNotification{
    //给AVPlayerItem添加播放完成通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem];
}

-(void)removeNotification{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

/**
 *  播放完成通知
 *
 *  @param notification 通知对象
 */
-(void)playbackFinished:(NSNotification *)notification{
    NSLog(@"视频播放完成.");
}

#pragma mark - 监控
/**
 *  给播放器添加进度更新
 */
-(void)addProgressObserver{
    AVPlayerItem *playerItem=self.player.currentItem;
    UIProgressView *progress=self.progress;
    //这里设置每秒执行一次
    [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        float current=CMTimeGetSeconds(time);
        float total=CMTimeGetSeconds([playerItem duration]);
        NSLog(@"当前已经播放%.2fs.",current);
        if (current) {
            [progress setProgress:(current/total) animated:YES];
        }
    }];
}

/**
 *  给AVPlayerItem添加监控
 *
 *  @param playerItem AVPlayerItem对象
 */
-(void)addObserverToPlayerItem:(AVPlayerItem *)playerItem{
    //监控状态属性,注意AVPlayer也有一个status属性,通过监控它的status也可以获得播放状态
    [playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    //监控网络加载情况属性
    [playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
}
-(void)removeObserverFromPlayerItem:(AVPlayerItem *)playerItem{
    [playerItem removeObserver:self forKeyPath:@"status"];
    [playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
}
/**
 *  通过KVO监控播放器状态
 *
 *  @param keyPath 监控属性
 *  @param object  监视器
 *  @param change  状态改变
 *  @param context 上下文
 */
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    AVPlayerItem *playerItem=object;
    if ([keyPath isEqualToString:@"status"]) {
        AVPlayerStatus status= [[change objectForKey:@"new"] intValue];
        if(status==AVPlayerStatusReadyToPlay){
            NSLog(@"正在播放...,视频总长度:%.2f",CMTimeGetSeconds(playerItem.duration));
        }
    }else if([keyPath isEqualToString:@"loadedTimeRanges"]){
        NSArray *array=playerItem.loadedTimeRanges;
        CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次缓冲时间范围
        float startSeconds = CMTimeGetSeconds(timeRange.start);
        float durationSeconds = CMTimeGetSeconds(timeRange.duration);
        NSTimeInterval totalBuffer = startSeconds + durationSeconds;//缓冲总长度
        NSLog(@"共缓冲:%.2f",totalBuffer);
//
    }
}

#pragma mark - UI事件
/**
 *  点击播放/暂停按钮
 *
 *  @param sender 播放/暂停按钮
 */
- (IBAction)playClick:(UIButton *)sender {
//    AVPlayerItemDidPlayToEndTimeNotification
    //AVPlayerItem *playerItem= self.player.currentItem;
    if(self.player.rate==0){ //说明时暂停
        [sender setImage:[UIImage imageNamed:@"player_pause"] forState:UIControlStateNormal];
        [self.player play];
    }else if(self.player.rate==1){//正在播放
        [self.player pause];
        [sender setImage:[UIImage imageNamed:@"player_play"] forState:UIControlStateNormal];
    }
}


/**
 *  切换选集,这里使用按钮的tag代表视频名称
 *
 *  @param sender 点击按钮对象
 */
- (IBAction)navigationButtonClick:(UIButton *)sender {
    [self removeNotification];
    [self removeObserverFromPlayerItem:self.player.currentItem];
    AVPlayerItem *playerItem=[self getPlayItem:sender.tag];
    [self addObserverToPlayerItem:playerItem];
    //切换视频
    [self.player replaceCurrentItemWithPlayerItem:playerItem];
    [self addNotification];
}

@end

到目前为止无论是MPMoviePlayerController还是AVPlayer来播放视频都相当强大,但是它也存在着一些不可回避的问题,那就是支持的视频编码格式很有限:H.264、MPEG-4,扩展名(压缩格式):.mp4、.mov、.m4v、.m2v、.3gp、.3g2等。但是无论是MPMoviePlayerController还是AVPlayer它们都支持绝大多数音频编码,所以大家如果纯粹是为了播放音乐的话也可以考虑使用这两个播放器。那么如何支持更多视频编码格式呢?目前来说主要还是依靠第三方框架,在iOS上常用的视频编码、解码框架有:VLC、ffmpeg, 具体使用方式今天就不再做详细介绍。

刚又看了一篇写的不错的文章,和大家分享下:http://www.cocoachina.com/ios/20160921/17609.html

如果我只是简单的播放一个视频,而不需要考虑播放器的界面。iOS9.0 之前使用 MPMoviePlayerController, 或者内部自带一个 view 的 MPMoviePlayerViewController. iOS9.0 之后,可以使用 AVPictureInPictureController, AVPlayerViewController, 或者 WKWebView。

以上系统提供的播放器由于高度的封装性, 使得自定义播放器变的很难。 所以,如果我需要自定义播放器样式的时候,可以使用 AVPlayer。 AVPlayer 存在于 AVFoundtion 中,更接近于底层,也更加灵活。

Representing and Using Media with AVFoundation

AVFoundtion 框架中主要使用 AVAsset 类来展示媒体信息,比如: title, duration, size 等等。

AVAsset : 存储媒体信息的一个抽象类,不能直接使用。

AVURLAsset : AVAsset 的一个子类,使用 URL 进行实例化,实例化对象包换 URL 对应视频资源的所有信息。

AVPlayerItem :  有一个属性为 asset。起到观察和管理视频信息的作用。 比如,asset, tracks , status, duration ,loadedTimeRange 等。

我的理解是, AVPlayItem 相当于 Model 层,包含 Media 的信息和播放状态,并提供这些数据给视频观察者 比如:属性 asset ,URL视频的信息. loadedTimeRanges ,已缓冲进度。

AVPlayerItem 使用
1. 初始化

playerItemWithURL 或者 initWithURL:

在使用 AVPlayer 播放视频时,提供视频信息的是 AVPlayerItem,一个 AVPlayerItem 对应着一个URL视频资源。

初始化一个 AVPlayItem 对象后,其属性并不是马上就可以使用。我们必须确保 AVPlayerItem 已经被加载好了,可以播放了,才能使用。 毕竟凡是和网络扯上关系的都需要时间去加载。 那么,什么时候属性才能正常使用呢。 官方文档给出了解决方案:

直到 AVPlayerItem 的 status 属性为 AVPlayerItemStatusReadyToPlay.

使用 KVO 键值观察者,其属性。

因此我们在使用的时候,使用 URL 初始化 AVPlayerItem 后,还要给它添加观察者。
2. 添加观察者

AVPlayreItem 的属性需要当 status 为 ReadyToPlay 的时候才可以正常使用。

观察status属性

[_playerItem addObserver:self forKeyPath:@”status” options:(NSKeyValueObservingOptionNew) context:nil]; // 观察status属性,

观察loadedTimeRanges

如果想做缓冲进度条,显示当前视频的缓存进度,则需要观察 loadedTimeRanges.

[_playerItem addObserver:self forKeyPath:@”loadedTimeRanges” options:NSKeyValueObservingOptionNew context:nil]; // 观察缓冲进度

AVPlayer & AVPlayerLayer

AVPlayer创建方式
AVPlayer 有三种创建方式:

init,initWithURL:,initWithPlayerItem: (URL,Item遍历构造器方法)

使用 AVPlayer 时需要注意,AVPlayer 本身并不能显示视频, 显示视频的是 AVPlayerLayer。 AVPlayerLayer 继承自 CALayer,添加到 view.layer 上就可以使用了。

AVPlayerLayer创建方式

AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
[superlayer addSublayer:playerLayer];

AVPlayerLayer 显示视频,AVPlayerItem 提供视频信息, AVPlayer 管理和调控。 这是不是非常熟悉。 我觉得这里也体现了 MVC 的思想(虽然AVPlayer继承自NSObject), 把响应层, 显示层, 信息层, 三层分离了。 明确了每层做的任务,使用起来就会更加得心应手。

使用 AVPlayer 的核心,在于 AVPlayer 和 AVPlayerItem, AVPlayerLayer 添加到视图的layer 上后,就没有什么事儿了。 思考一下,整个播放视频的步骤。

首先,得到视频的URL

根据URL创建AVPlayerItem

把AVPlayerItem 提供给 AVPlayer

AVPlayerLayer 显示视频。

AVPlayer 控制视频, 播放, 暂停, 跳转 等等。

播放过程中获取缓冲进度,获取播放进度。

视频播放完成后做些什么,是暂停还是循环播放,还是获取最后一帧图像。

播放步骤
1. 布局页面,初始化 AVPlayer 和 AVPlayerLayer

// setAVPlayer
self.player = [[AVPlayer alloc] init];
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
[self.playerView.layer addSublayer:_playerLayer];

2. 根据 URL 获取 AVPayerItem,并替换 AVPlayer 的 AVPlayerItem

在第一步,布局初始化时,AVPlayer 并没有 AVPlayerItem,AVPlayer 提供了 - (void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item; 方法,用于切换视频。

  • (void)updatePlayerWithURL:(NSURL *)url {
    _playerItem = [AVPlayerItem playerItemWithURL:url]; // create item
    [_player replaceCurrentItemWithPlayerItem:_playerItem]; // replaceCurrentItem
    [self addObserverAndNotification]; // 注册观察者,通知
    }

    1. KVO 获取视频信息, 观察缓冲进度

观察 AVPlayerItem 的 status 属性,当状态变为 AVPlayerStatusReadyToPlay 时才可以使用。

也可以观察 loadedTimeRanges 获取缓冲进度

注册观察者:

[_playerItem addObserver:self forKeyPath:@”status” options:(NSKeyValueObservingOptionNew) context:nil]; // 观察status属性

执行观察者方法:

  • (void)observeValueForKeyPath:(NSString )keyPath ofObject:(id)object change:(NSDictionary )change context:(void *)context {
    AVPlayerItem item = (AVPlayerItem )object;
    if ([keyPath isEqualToString:@”status”]) {
    AVPlayerStatus status = [[change objectForKey:@”new”] intValue]; // 获取更改后的状态
    if (status == AVPlayerStatusReadyToPlay) {
    CMTime duration = item.duration; // 获取视频长度
    // 设置视频时间
    [self setMaxDuration:CMTimeGetSeconds(duration)];
    // 播放
    [self play];
    } else if (status == AVPlayerStatusFailed) {
    NSLog(@”AVPlayerStatusFailed”);
    } else {
    NSLog(@”AVPlayerStatusUnknown”);
    }

    } else if ([keyPath isEqualToString:@”loadedTimeRanges”]) {
    NSTimeInterval timeInterval = [self availableDurationRanges]; // 缓冲时间
    CGFloat totalDuration = CMTimeGetSeconds(_playerItem.duration); // 总时间
    [self.loadedProgress setProgress:timeInterval / totalDuration animated:YES]; // 更新缓冲条
    }
    }

    1. 播放过程中响应:播放、 暂停、 跳转

AVPlayer 提供了 play , pause, 和 - (void)seekToTime:(CMTime)time completionHandler:(void (^)(BOOL finished))completionHandler 方法。

在看 AVPlayer 的 seekToTime 之前,先来认识一个结构体。

CMTime 是专门用于标识电影时间的结构体.

typedef struct{
CMTimeValue value; // 帧数
CMTimeScale timescale; // 帧率(影片每秒有几帧)
CMTimeFlags flags;
CMTimeEpoch epoch;
} CMTime;

AVPlayerItem 的 duration 属性就是一个 CMTime 类型的数据。 如果我们想要获取影片的总秒数那么就可以用 duration.value / duration.timeScale 计算出来。也可以使用 CMTimeGetSeconds 函数

CMTimeGetSeconds(CMtime time)
double seconds = CMTimeGetSeconds(item.duration); // 相当于 duration.value / duration.timeScale

如果一个影片为60frame(帧)每秒, 当前想要跳转到 120帧的位置,也就是两秒的位置,那么就可以创建一个 CMTime 类型数据。

CMTime,通常用如下两个函数来创建.

CMTimeMake(int64_t value, int32_t scale)
CMTime time1 = CMTimeMake(120, 60);

CMTimeMakeWithSeconds(Flout64 seconds, int32_t scale)
CMTime time2 = CMTimeWithSeconds(120, 60);

CMTimeMakeWithSeconds 和CMTimeMake 区别在于,第一个函数的第一个参数可以是float,其他一样。

拖拽方法如下:

  • (IBAction)playerSliderValueChanged:(id)sender {
    _isSliding = YES;
    [self pause]; // 跳转到拖拽秒处
    // self.playProgress.maxValue = value / timeScale
    // value = progress.value * timeScale
    // CMTimemake(value, timeScale) = (progress.value, 1.0)
    CMTime changedTime = CMTimeMakeWithSeconds(self.playProgress.value, 1.0);
    [_playerItem seekToTime:changedTime completionHandler:^(BOOL finished) {
    // 跳转完成后
    }];
    }

    1. 观察 AVPlayer 播放进度

AVPlayerItem 是使用 KVO 模式观察状态,和 缓冲进度。而 AVPlayer 给我们直接提供了 观察播放进度更为方便的方法。

  • (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;

方法名如其意, “添加周期时间观察者” ,参数1 interal 为CMTime 类型的,参数2 为一个 返回值为空,参数为 CMTime 的block类型。

简而言之就是,每隔一段时间后执行 block。

比如: 我把时间间隔设置为, 1/ 30 秒,然后 block 里面更新 UI。就是一秒钟更新 30次UI。

播放进度代码如下:

// 观察播放进度
- (void)monitoringPlayback:(AVPlayerItem *)item {
__weak typeof(self)WeakSelf = self;

// 观察间隔, CMTime 为30分之一秒
_playTimeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 30.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
    if (_touchMode != TouchPlayerViewModeHorizontal) {
        // 获取 item 当前播放秒
        float currentPlayTime = (double)item.currentTime.value/ item.currentTime.timescale;
        // 更新slider, 如果正在滑动则不更新
        if (_isSliding == NO) {
            [WeakSelf updateVideoSlider:currentPlayTime];
        }
    } else {
        return;
    }
}];

}

注意: 给 palyer 添加了 timeObserver 后,不使用的时候记得移除 removeTimeObserver 否则会占用大量内存。

比如,我在dealloc里面做了移除:

  • (void)dealloc {
    [self removeObserveAndNOtification];
    [_player removeTimeObserver:_playTimeObserver]; // 移除playTimeObserver}

    1. AVPlayerItem 通知

AVPlaerItem 播放完成后,系统会自动发送通知,通知的定义详情请见 AVPlayerItem.h.

/* Note that NSNotifications posted by AVPlayerItem may be posted on a different thread from the one on which the observer was registered. */

// notifications description
AVF_EXPORT NSString *const AVPlayerItemTimeJumpedNotification NS_AVAILABLE(10_7, 5_0); // the item’s current time has changed discontinuously
AVF_EXPORT NSString *const AVPlayerItemDidPlayToEndTimeNotification NS_AVAILABLE(10_7, 4_0); // item has played to its end time
AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeNotification NS_AVAILABLE(10_7, 4_3); // item has failed to play to its end time
AVF_EXPORT NSString *const AVPlayerItemPlaybackStalledNotification NS_AVAILABLE(10_9, 6_0); // media did not arrive in time to continue playback
AVF_EXPORT NSString *const AVPlayerItemNewAccessLogEntryNotification NS_AVAILABLE(10_9, 6_0); // a new access log entry has been added
AVF_EXPORT NSString *const AVPlayerItemNewErrorLogEntryNotification NS_AVAILABLE(10_9, 6_0); // a new error log entry has been added

// notification userInfo key type
AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeErrorKey NS_AVAILABLE(10_7, 4_3); // NSError

因此,如果我们想要在某个状态下,执行某些操作。监听 AVPlayerItem 的相关通知就行了。 比如,我想要播放完成后,暂停播放。 给AVPlayerItemDidPlayToEndTimeNotification 添加观察者。

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];

// 播放完成后
- (void)playbackFinished:(NSNotification *)notification {
NSLog(@”视频播放完成通知”);
_playerItem = [notification object];
[_playerItem seekToTime:kCMTimeZero]; // item 跳转到初始
//[_player play]; // 循环播放
}

最后

使用 AVPlayer 的时候,一定要注意 AVPlayer 、 AVPlayerLayer 和 AVPlayerItem 三者之间的关系。 AVPlayer 负责控制播放, layer 显示播放, item 提供数据,当前播放时间, 已加载情况。 Item 中一些基本的属性, status, duration, loadedTimeRanges, currentTime(当前播放时间)。
当然,如果我写的文章有幸让你看到了最后,那么, 或许 你想要更多的功能。比如,横竖屏旋转,一些交互动画等等。 我有个简单地 demo,实现了一些小小的功能,放到了 github 上, 里面还有很多不足,多沟通交流 = W = 。

githubDemo地址

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值