Android高仿抖音照片電影功能

本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布

PhotoMovie(https://github.com/yellowcath/PhotoMovie)可輕松實(shí)現(xiàn)類似抖音女蜈、微視喘蟆、美拍的照片電影功能。效果如下

濾鏡效果

filter.gif

轉(zhuǎn)場(chǎng)效果

transfer.gif

基本用法

可參照DemoPresenter

        //添加圖片
        List<PhotoData> photoDataList = new LinkedList<PhotoData>();
        photoDataList.add(new SimplePhotoData(context,photoPath1,PhotoData.STATE_LOCAL));
        ...
        photoDataList.add(new SimplePhotoData(context,photoPathN,PhotoData.STATE_LOCAL));
        //生成圖片源
        PhotoSource photoSource = new PhotoSource(photoDataList);
        //生成照片電影(使用預(yù)定義的水平轉(zhuǎn)場(chǎng)動(dòng)畫)
        PhotoMovie photoMovie = PhotoMovieFactory.generatePhotoMovie(photoSource, PhotoMovieFactory.PhotoMovieType.HORIZONTAL_TRANS);
        //生成負(fù)責(zé)繪制電影內(nèi)容的MovieRenderer
        MovieRenderer movieRenderer = new GLTextureMovieRender(glTextureView);
        /**
         * OR  MovieRenderer movieRenderer = new GLSurfaceMovieRenderer(glSurfaceView);
         */
        //照片電影播放器
        PhotoMoviePlayer photoMoviePlayer = new PhotoMoviePlayer(context);
        photoMoviePlayer.setMovieRenderer(mMovieRenderer);
        photoMoviePlayer.setMovieListener(...);
        photoMoviePlayer.setLoop(true);
        photoMoviePlayer.setOnPreparedListener(new PhotoMoviePlayer.OnPreparedListener() {
            @Override
            public void onPreparing(PhotoMoviePlayer moviePlayer, float progress) {
            }

            @Override
            public void onPrepared(PhotoMoviePlayer moviePlayer, int prepared, int total) {
                 mPhotoMoviePlayer.start();
            }

            @Override
            public void onError(PhotoMoviePlayer moviePlayer) {
            }
        });
        photoMoviePlayer.prepare();

輕松擴(kuò)展

PhotoMovie使用模塊化的設(shè)計(jì)唯笙,每個(gè)部分都可以自定義然后替換私痹,主要類圖如下


PhotoMovie.png

鱼填,在這段時(shí)間之內(nèi)以特定的方式播放圖片,例如ScaleSegment會(huì)對(duì)圖片做縮放動(dòng)畫喉祭、EndGaussianBlurSegment會(huì)對(duì)圖片做從清晰到模糊的高斯模糊動(dòng)畫

  • PhotoMovie:核心類盖喷,代表照片電影本身爆办,由圖片源(PhotoSource)和若干電影片段(MovieSegment)組成一個(gè)完整的照片電影,圖片通過PhotoAllocator分配給MovieSegment

  • MovieLayer:為MovieSegment擴(kuò)展繪制多層特效的功能课梳,例如SubtitleLayer提供字幕展示

  • IMovieFilter:為整個(gè)照片電影提供濾鏡

  • MovieRenderer:負(fù)責(zé)把照片電影渲染到指定的輸出界面距辆,例如TextureView(GLTextureMovieRender)余佃、GLSurfaceView(GLSurfaceMovieRenderer)

  • PhotoMoviePlayer:提供類似MediaPlayer的接口,負(fù)責(zé)播放照片電影,播放進(jìn)度由IMovieTimer控制

擴(kuò)展電影類型

目前內(nèi)置了6種類型,后兩種即是抖音的左右切換和上下切換跨算,Thaw和WINDOW仿自美拍

 public enum PhotoMovieType {
        THAW,  //融雪
        SCALE, //縮放
        SCALE_TRANS, //縮放 & 平移
        WINDOW, //窗扉
        HORIZONTAL_TRANS,//橫向平移
        VERTICAL_TRANS//縱向平移
    }

這里以微視的漸變特效為例展示如何擴(kuò)展
分析得出咙冗,漸變特效首先圖片居中放置,然后全程做一個(gè)微弱的放大動(dòng)畫漂彤,后半部分同時(shí)透明度變化消失
,更直觀的流程如下圖

gradient_timeline.png

可見需要兩個(gè)不同的片段類型
首先創(chuàng)建FitCenterScaleSegment,繼承FitCenterSegment灾搏,實(shí)現(xiàn)單張圖片的放大動(dòng)畫

public class FitCenterScaleSegment extends FitCenterSegment {
    /**
     * 縮放動(dòng)畫范圍
     */
    private float mScaleFrom;
    private float mScaleTo;

    private float mProgress;

    /**
     * @param duration  片段時(shí)長(zhǎng)
     * @param scaleFrom 縮放范圍
     * @param scaleTo   縮放范圍
     */
    public FitCenterScaleSegment(int duration, float scaleFrom, float scaleTo) {
        super(duration);
        mScaleFrom = scaleFrom;
        mScaleTo = scaleTo;
    }

    @Override
    protected void onDataPrepared() {
        super.onDataPrepared();
    }

    @Override
    public void drawFrame(GLESCanvas canvas, float segmentProgress) {
        mProgress = segmentProgress;
        if (!mDataPrepared) {
            return;
        }
        drawBackground(canvas);
        float scale = mScaleFrom + (mScaleTo - mScaleFrom) * mProgress;
        //FitCenterSegment已經(jīng)具有縮放能力挫望,這里傳縮放值即可
        drawContent(canvas, scale);
    }
    //提升這兩個(gè)函數(shù)的訪問權(quán)限,供轉(zhuǎn)場(chǎng)時(shí)使用
        @Override
    public void drawContent(GLESCanvas canvas, float scale) {
        super.drawContent(canvas, scale);
    }

    @Override
    public void drawBackground(GLESCanvas canvas) {
        super.drawBackground(canvas);
    }
}

然后創(chuàng)建轉(zhuǎn)場(chǎng)片段GradientTransferSegment,其父類TransitionSegment同時(shí)持有上一個(gè)與下一個(gè)片段狂窑,
可以在其基礎(chǔ)上實(shí)現(xiàn)任意轉(zhuǎn)場(chǎng)功能

public class GradientTransferSegment extends TransitionSegment<FitCenterScaleSegment, FitCenterScaleSegment> {
    /**
     * 縮放動(dòng)畫范圍
     */
    private float mPreScaleFrom;
    private float mPreScaleTo;
    private float mNextScaleFrom;
    private float mNextScaleTo;

    public GradientTransferSegment(int duration,
                                   float preScaleFrom, float preScaleTo,
                                   float nextScaleFrom, float nextScaleTo) {
        mPreScaleFrom = preScaleFrom;
        mPreScaleTo = preScaleTo;
        mNextScaleFrom = nextScaleFrom;
        mNextScaleTo = nextScaleTo;
        setDuration(duration);
    }

    @Override
    protected void onDataPrepared() {

    }

    @Override
    public void drawFrame(GLESCanvas canvas, float segmentProgress) {
        //下一個(gè)片段開始放大
        float nextScale = mNextScaleFrom + (mNextScaleTo - mNextScaleFrom) * segmentProgress;
        mNextSegment.drawContent(canvas, nextScale);

        //上一個(gè)片段繼續(xù)放大同時(shí)變透明
        float preScale = mPreScaleFrom + (mPreScaleTo - mPreScaleFrom) * segmentProgress;
        float alpha = 1 - segmentProgress;
        mPreSegment.drawBackground(canvas);
        canvas.save();
        canvas.setAlpha(alpha);
        mPreSegment.drawContent(canvas, preScale);
        canvas.restore();
    }

創(chuàng)建照片電影

    private static PhotoMovie initGradientPhotoMovie(PhotoSource photoSource) {
        List<MovieSegment> segmentList = new ArrayList<>(photoSource.size());
        for (int i = 0; i < photoSource.size(); i++) {
            if (i == 0) {
                segmentList.add(new FitCenterScaleSegment(1600, 1f, 1.1f));
            } else {
                segmentList.add(new FitCenterScaleSegment(1600, 1.05f, 1.1f));
            }
            if (i < photoSource.size() - 1) {
                segmentList.add(new GradientTransferSegment(800, 1.1f, 1.15f, 1.0f, 1.05f));
            }
        }
        return new PhotoMovie(photoSource, segmentList);
    }

然后將這個(gè)PhotoMovie正常播放即可媳板,效果如下

gradient.gif

擴(kuò)展濾鏡

目前內(nèi)置了9個(gè)濾鏡

public enum  FilterType {
    NONE,
    CAMEO,//浮雕
    GRAY,//黑白
    KUWAHARA,//水彩
    SNOW,//飄雪(動(dòng)態(tài))
    LUT1,
    LUT2,
    LUT3,
    LUT4,
    LUT5,
}

先看IMovieFilter

public interface IMovieFilter {
    void doFilter(PhotoMovie photoMovie,int elapsedTime, FboTexture inputTexture, FboTexture outputTexture);
    void release();
}

外部會(huì)提供一個(gè)輸入紋理,然后由IMovieFilter處理之后繪制到輸出紋理上泉哈,即實(shí)現(xiàn)了濾鏡效果

BaseMovieFilter已經(jīng)實(shí)現(xiàn)了基本的輸入輸出流程蛉幸,例如要做最基本的黑白濾鏡,只需更換FRAGMENT_SHADER即可

public class GrayMovieFilter extends BaseMovieFilter {
    protected static final String FRAGMENT_SHADER = "" +
            "varying highp vec2 textureCoordinate;\n" +
            " \n" +
            "uniform sampler2D inputImageTexture;\n" +
            " \n" +
            "void main()\n" +
            "{\n" +
            "     mediump vec4 color = texture2D(inputImageTexture, textureCoordinate);\n" +
            "     mediump float gray = color.r*0.3+color.g*0.59+color.b*0.11;\n"+
            "     gl_FragColor = vec4(gray,gray,gray,1.0);\n"+
            "}";
    public GrayMovieFilter(){
        super(VERTEX_SHADER,FRAGMENT_SHADER);
    }
}

同時(shí)PhotoMovie提供了對(duì)Lut濾鏡的支持

Lut其實(shí)就是Lookup Table(顏色查找表)丛晦,根據(jù)原圖的RGB值去相應(yīng)的lut圖里面查找對(duì)應(yīng)轉(zhuǎn)換后的RGB值奕纫,從而實(shí)現(xiàn)各種濾鏡效果

lut 效果
lut(原圖)
原圖
lut_2.jpg
濾鏡效果圖
public class LutMovieFilter extends TwoTextureMovieFilter {

    public LutMovieFilter(Bitmap lutBitmap){
        super(loadShaderFromAssets("shader/two_vertex.glsl"),loadShaderFromAssets("shader/lut.glsl"));
        setBitmap(lutBitmap);
    }
}

在LutMovieFilter的構(gòu)造函數(shù)傳入上面表格里的lut圖,即可實(shí)現(xiàn)相應(yīng)的濾鏡效果,前面提到的黑白濾鏡也可用這個(gè)方式實(shí)現(xiàn)

錄制功能

GLMovieRecorder提供了將照片電影錄制為mp4的功能

可參照DemoPresenter的saveVideo()函數(shù)

        GLMovieRecorder recorder = new GLMovieRecorder();
        recorder.configOutput(width, height(), bitrate,frameRate,iFrameInterval, outputPath);
        recorder.setDataSource(movieRenderer);
        recorder.startRecord(new GLMovieRecorder.OnRecordListener() {
            @Override
            public void onRecordFinish(boolean success) {
               ......
            }

            @Override
            public void onRecordProgress(int recordedDuration, int totalDuration) {
               ......
            }
        });

背景音樂

 mPhotoMoviePlayer.setMusic(context, mMusicUri);

PhotoMovie只提供了播放背景音樂的功能烫沙,錄制完成之后需自行合成匹层,Demo里使用了
VideoProcessor進(jìn)行合成

 VideoProcessor.mixAudioTrack(context, videPath, audioPath,outputPath, null, null, 0,100, 1f, 1f);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市锌蓄,隨后出現(xiàn)的幾起案子升筏,更是在濱河造成了極大的恐慌,老刑警劉巖瘸爽,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件您访,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡剪决,警方通過查閱死者的電腦和手機(jī)灵汪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)昼捍,“玉大人识虚,你說我怎么就攤上這事《什纾” “怎么了担锤?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)乍钻。 經(jīng)常有香客問我肛循,道長(zhǎng)铭腕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任多糠,我火速辦了婚禮累舷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘夹孔。我一直安慰自己被盈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布搭伤。 她就那樣靜靜地躺著只怎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪怜俐。 梳的紋絲不亂的頭發(fā)上身堡,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音拍鲤,去河邊找鬼贴谎。 笑死,一個(gè)胖子當(dāng)著我的面吹牛季稳,可吹牛的內(nèi)容都是我干的擅这。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼绞幌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蕾哟!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起莲蜘,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤谭确,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后票渠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逐哈,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年问顷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了昂秃。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡杜窄,死狀恐怖肠骆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情塞耕,我是刑警寧澤蚀腿,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響莉钙,放射性物質(zhì)發(fā)生泄漏廓脆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一磁玉、第九天 我趴在偏房一處隱蔽的房頂上張望停忿。 院中可真熱鬧,春花似錦蚊伞、人聲如沸席赂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)氧枣。三九已至,卻和暖如春别垮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背扎谎。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工碳想, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人毁靶。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓胧奔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親预吆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子龙填,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫(kù)組件 SD...
    陽(yáng)明先生_X自主閱讀 15,982評(píng)論 3 119
  • 第一個(gè)說過第一個(gè)說過第一
    Ara_a396閱讀 88評(píng)論 0 0
  • 1.加爾文關(guān)注的不是教皇里的圣經(jīng)拐叉;不是人按傳統(tǒng)岩遗,而是依靠圣經(jīng)。想對(duì)圣禮有明確的認(rèn)識(shí)凤瘦;真正的教導(dǎo)和牧養(yǎng)的明確宿礁。 2對(duì)...
    柴火妞兒Dorcas閱讀 229評(píng)論 0 0
  • 2018年6月30日 星期六 晴 親子日記第189篇 三年級(jí)二班張家俊媽媽 今天兒子完成作業(yè)非常早,自由時(shí)間安...
    隨緣_子洲媽媽閱讀 230評(píng)論 0 0