Android高仿抖音照片電影功能

郭霖2018-09-25 09:09:00



今日科技快訊


9月17日,據路透社報道,京東CEO劉強東將不參加本週在上海舉辦的2018世界人工智能大會。此前,劉強東8月31日因涉嫌性侵在美國明尼蘇達州被捕,第二天被釋放,但仍被美國警方調查,他通過他的律師否認有任何不法行為,並重返中國工作。


作者簡介


本篇轉自 yellowcath 的博客,詳細地解析了模擬抖音的功要能用到的模塊 一起來看看!希望大家喜歡。

yellowcath 的博客地址:

https://blog.csdn.net/yellowcath/article/details/82664987


前言


PhotoMovie,可輕鬆實現類似抖音、微視、美拍的照片電影功能。效果如下

轉場效果


基本用法


可參照 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);
        //生成照片電影(使用預定義的水平轉場動畫)
        PhotoMovie photoMovie = PhotoMovieFactory.generatePhotoMovie(photoSource, PhotoMovieFactory.PhotoMovieType.HORIZONTAL_TRANS);
        //生成負責繪製電影內容的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();


輕鬆擴展


PhotoMovie 使用模塊化的設計,每個部分都可以自定義然後替換,主要類圖如下

  • MovieSegment: 電影片段,每個電影片段都有特定的時長,在這段時間之內以特定的方式播放圖片,例如 ScaleSegment 會對圖片做、EndGaussianBlurSegment 會對圖片做從清晰到模糊的高斯模糊動畫

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

  • MovieLayer: 為 MovieSegment 擴展繪製多層特效的功能,例如 SubtitleLayer 提供字幕展示

  • IMovieFilter: 為整個照片電影提供濾鏡

  • MovieRenderer: 負責把照片電影渲染到指定的輸出界面,例如TextureView(GLTextureMovieRender)、GLSurfaceView(GLSurfaceMovieRenderer)

  • PhotoMoviePlayer: 提供類似 MediaPlayer 的接口,負責播放照片電影,播放進度由IMovieTimer 控制

擴展電影類型

目前內置了6種類型,後兩種即是抖音的左右切換和上下切換,Thaw 和 WINDOW 仿自美拍

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

這裏以微視的漸變特效為例展示如何擴展。分析得出,漸變特效首先圖片居中放置,然後全程做一個微弱的放大動畫,後半部分同時透明度變化消失 ,更直觀的流程如下圖

可見需要兩個不同的片段類型 首先創建 FitCenterScaleSegment,繼承 FitCenterSegment,實現單張圖片的放大動畫

public class FitCenterScaleSegment extends FitCenterSegment {
    /**
     * 縮放動畫範圍
     */

    private float mScaleFrom;
    private float mScaleTo;

    private float mProgress;

    /**
     * @param duration  片段時長
     * @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已經具有縮放能力,這裏傳縮放值即可
        drawContent(canvas, scale);
    }
    //提升這兩個函數的訪問權限,供轉場時使用
        @Override
    public void drawContent(GLESCanvas canvas, float scale) {
        super.drawContent(canvas, scale);
    }

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

然後創建轉場片段 GradientTransferSegment,其父類 TransitionSegment 同時持有上一個與下一個片段, 可以在其基礎上實現任意轉場功能

public class GradientTransferSegment extends TransitionSegment<FitCenterScaleSegmentFitCenterScaleSegment{
    /**
     * 縮放動畫範圍
     */

    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) {
        //下一個片段開始放大
        float nextScale = mNextScaleFrom + (mNextScaleTo - mNextScaleFrom) * segmentProgress;
        mNextSegment.drawContent(canvas, nextScale);

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

創建照片電影

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(16001f1.1f));
            } else {
                segmentList.add(new FitCenterScaleSegment(16001.05f1.1f));
            }
            if (i < photoSource.size() - 1) {
                segmentList.add(new GradientTransferSegment(8001.1f1.15f1.0f1.05f));
            }
        }
        return new PhotoMovie(photoSource, segmentList);
    }

然後將這個 PhotoMovie 正常播放即可,效果如下

擴展濾鏡

目前內置了9個濾鏡

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

先看 IMovieFilter

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

外部會提供一個輸入紋理,然後由 IMovieFilter 處理之後繪製到輸出紋理上,即實現了濾鏡效果。BaseMovieFilter 已經實現了基本的輸入輸出流程,例如要做最基本的黑白濾鏡,只需更換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);
    }
}

同時PhotoMovie提供了對Lut濾鏡的支持

Lut其實就是Lookup Table(顏色查找表),根據原圖的 RGB 值去相應的 lut 圖裏面查找對應轉換後的 RGB 值,從而實現各種濾鏡效果

下面表格第一行為原圖

public class LutMovieFilter extends TwoTextureMovieFilter {

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

在 LutMovieFilter 的構造函數傳入上面表格裏的lut圖,即可實現相應的濾鏡效果,前面提到的黑白濾鏡也可用這個方式實現。


錄製功能


GLMovieRecorder 提供了將照片電影錄製為 mp4 的功能。可參照 DemoPresenter 的 saveVideo() 函數

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 進行合成

VideoProcessor.mixAudioTrack(context, videPath, audioPath,outputPath, nullnull0,1001f1f);


總結


本文我們學習了抖音的照片電影功能,有任何疑問歡迎聯繫。


    歡迎長按下圖 -> 識別圖中二維碼

    或者 掃一掃 關注我的公眾號

閲讀原文

TAGS: