ViewGroup et ScreenRecorder, fais toi plaisir.

b2ap3_thumbnail_Android2ee-logo-BIG_blue-1024x375.png

 Bonjour,

"Putain qu'il fait chaud" c'est la pensée du mois et sinon ? Et sinon, je suis en train de mettre à jour mon chapitre de formation sur les animations. Oui, pour parler des Drawable qui bougent tous seuls, des ViewPropertiesAnimator, des TweensAnimations, de scene et transition, des nouveaux VectorDrawableAnimator (que je pensais que ce serait super et que je suis super déçu :(... )

"D'accord mathias et alors ?" allez vous me dire. Et alors, il me faut faire des captures d'écran d'animation, genre j'en veux 30 par seconde, ou 15 ou 60, c'est celon. J'ai essayé quelques outils et je trouve ça compliqué pour pas grand chose... Alors je me suis dit, non mais oh, c'est quand même trivial de faire des copies d'écrans, je vais le mettre en place et ça va me le faire direct. Et c'est ce dont j'ai envie de vous parler aujourd'hui.

Tout d'abord, mon idée est de se dire que le layout que je vais utiliser a la capacité de prendre tout seul, quand je le lui demande, des captures d'écrans en raphale. Toute bête comme idée, il me suffira ensuite d'aller récupérer mes png sur l'émulateur ou le device qui exécute mon code. Je vais juste vous raconter les deux trois problèmes que j'ai rencontrés.

Mais commençons par le principe : Pour faire une capture d'écran, il suffit de créer un Bitmap que l'on utilise pour créer un Canvas. On passe ce Canvas à la méthode qui dessine le composant pour que le composant se dessine dedans et ainsi se dessine dans le bitmap. Quand ça s'est fait, il suffit de sauvegarder le Bitmap dans le dossier des images publiques du téléphone et le tour est joué. Avec cet algorithme en tête, je me suis dit, dans une heure c'est fait...

Tout d'abord, première étape, je souhaite enregistrer mon png sur mon téléphone et voir les captures d'écrans apparaître dans mon browser de photos. On y va, on génére la photo (je vous montrerai le code plus bas), on l'enregistre dans le repertoire public, en lui disant que c'est une photo. Pour cela, la bonne pratique est d'utiliser la classe File et de récupérer le root folder du dossier public:

File filesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);

Dans ce root folder, on créé un sous-repertoire et on y stocke notre fichier. Première connerie, j'ai oublié de mettre png comme extension à mon fichier, ça s'est purement android, on ne met jamais les extensions de nos fichiers quand on les utilise... Ben là, ta photo, tu risques pas de la voir apparaitre dans gallery, ni d'ailleurs dans ton explorateur de fichiers. Je finis par percuter, je mets mon extension. En me battant avec mon téléphone, je finis pas voir mon fichier, mais ce n'est pas naturel et l'application Gallery (la visionneuse de photos) ne voit pas mes fichiers, ni l'explorateur de fichiers sur mon ordinateur quand je parcours le téléphone... Arf, j'ai oublié de prevenir le ContentProvider qui gère les images de rajouter un dossier/mon fichier dans son scope. Il faut donc le lui dire:

MediaScannerConnection.scanFile(getContext(), new String[]{filePicture.getAbsolutePath()}, null, 
new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
Log.e("LinearLayoutScreenRec", "Scan done for the path : " + path);
}
});

Et là, enfin, mon fichier apparaît naturellement et dans Gallery et dans l'explorateur de fichiers de l'ordinateur quand je parcours le téléphone. Yes ! Moi qui pensait faire le truc en 20 minutes maximum, ça fait déjà 1 heure que je bataille... En même temps j'ai tout fait d'un coup, la capture d'écran, la sauvegarde sur le téléphone, la thread, le handler ... et je vois pas trop ou se situe les problèmes dans ce sac de noeuds... d'autant que je sais pas pourquoi, mais j'étais dans le gaz hier matin... (c'était Dimanche matin, hier)...

J'ai plus qu'à réussir à faire cette capture d'écran qui veut pas se faire, pourtant je surcharge bien onDraw(Canvas canvas), c'était le souvenir que j'avais... Et puis, va savoir pourquoi ou comment, je percute enfin, ce n'est pas onDraw qu'il faut surcharger, mais draw(Canvas canvas) et là tout d'un coup j'arrive enfin à générer mes captures d'écrans. Youpi :)

Bon y'a plus qu'a être propre... Ben oui, le Bitmap il faut bien le gérer, son enregistrement aussi, le déclencher quand il faut, le recycler ne pas s'embrouiller les Threads, qu'est ce qui prend du temps la génération du Bitmap ou son écriture sur disque ?... Bref, vous savez, le temps qu'on prend quand enfin ça marche pour que ce soit propre.

Première remarque, c'est l'enregistrement sur le disque qui prend du temps (1110 ms en moyenne pour un nexus 5 et un fichier de 100 ko) alors que la génération du Bitmap ne prends que 50ms...

Seconde remarque, je ne peux demander la libération de la mémoire du bitmap que quand j'ai écrit le dit bitmap sur disque mais j'ai besoin d'un bitmap quand je souhaite capturer l'écran. Je ne peux donc pas ré-utiliser mon bitmap (ou alors je pars sur la mise en place d'un pool de bitmap partagé entre deux Threads que je libère quand mon application se termine... sauf que je souhaite mettre à jour mon cours par sortir une librairie...). De plus, à chaque enregistrement du bitmap je créé une nouvelle Thread que j'exécute et qui prend en charge l'écriture sur disque d'un bitmap. Je préferre cette stratégie à celle de générer tous les bitmaps puis de les écrire par la suite tous d'un bloc dans une Thread. Et je ne mets pas en place un ExecutorService non plus par ce qu'il faut l'arreter...

Et donc, il ne faut utiliser ce code qu'en mode DEBUG. En effet, le garbage collector va être stressé par la libération de ces bitmaps, de ces Threads et la prise d'une capture d'écrans prend trop de temps. Je sais que des frames vont être ratées et donc cela va saccader mon écran. Même si j'écris mes bitmaps dans une Thread de background.

Ainsi ma méthode draw(Canvas canvas) ressemble à cela :

@Override
public void draw(Canvas canvas) {
if(canvas!=null){
super.draw(canvas);
}
if(screenCaptureOn) {
skippedFrames++;
if (skippedFrames == skipFrames) {
debut= System.currentTimeMillis();
Log.e("LinearLayoutScreenRec", "onDraw screenCaptureOn skippedFrames" + skippedFrames);
mCurrentBitmap = Bitmap.createBitmap(layoutW, layoutH, Bitmap.Config.ARGB_8888);
mBitmapCanvas = new Canvas(mCurrentBitmap);
mBitmapCanvas.drawRect(0, 0, layoutW, layoutH, paint);
super.draw(mBitmapCanvas);
recordedFrameNum++;
new Thread(new saveOnDiskRunnable(mCurrentBitmap, recordedFrameNum)).start();
fin = System.currentTimeMillis();
//using this method take -1091milli
Log.e("LinearLayoutScreenRec", "saveBitmap finished in " + (debut - fin));
skippedFrames = 0;
}
}
}

Ben voilà, ça y est enfin, enfin presque, il me faut déclencher tout ce beau monde au bon moment, du coup, j'ai besoin d'un handler et d'un Runnable qui vont me servir à déclencher draw :

/**
* The Handler to run the recordRunnable in the UI thread
*/
Handler recorderHandler =new Handler();
/**
* The Runnbale to record
*/
Runnable recorderRunnable =new Runnable() {
@Override
public void run() {
draw(null);
recorderHandler.postDelayed(this, 32);
}
};

 Ce qui donne :

 

/**
* Created by Mathias Seguy - Android2EE on 16/07/2015.
* This LinearLayout is a LinearLayout that can take screen capture at the rate you want
* and store those captures in your pictures public directory.
* The name of the files saved on the disk is the tag of the linearLayout defined in your xml
* with the suffix of the number of the screen capture
*/
public class LinearLayoutScreenRecorder extends LinearLayout {
/**The layout width and heigth*/
int layoutW, layoutH;
/** The screen capture is active*/
boolean screenCaptureOn = false;
/** The canvas to draw within to generate the bitmap*/
Canvas mBitmapCanvas;
/** The bitmap handling the screen capture*/
Bitmap mCurrentBitmap;
/** The root folder to store the picture inside*/
File mRootFolder = null;
/**Paint used to draw the background */
Paint paint;

/***********************************************************
* Constructors
**********************************************************/
public LinearLayoutScreenRecorder(Context context) {
super(context);
initiliazePaint();
}
public LinearLayoutScreenRecorder(Context context, AttributeSet attrs) {
super(context, attrs);
initiliazePaint();
}
public LinearLayoutScreenRecorder(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initiliazePaint();
}
public LinearLayoutScreenRecorder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initiliazePaint();
}
/***
* Initiliaze the Paint
*/
private void initiliazePaint() {
paint = new Paint();
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setAntiAlias(true);
}
/***********************************************************
* The magic method that tells us we know the size of the layout
**********************************************************/
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//so here are our size
layoutH = h;
layoutW = w;
}

/***********************************************************
* The public methods start and stop recording
* And others Parameters
**********************************************************/

/**
* Start recording the screen
*/
public void startRecording() {
//only do it in debug mode
if(BuildConfig.DEBUG) {
screenCaptureOn = true;
recorderHandler.postDelayed(recorderRunnable, 32);
}
}

/**
* Stop recording the screen
* Insure it has been called when the onPause method has finished
*/
public void stopRecording() {
//only do it in debug mode
if(BuildConfig.DEBUG) {
screenCaptureOn = false;
recorderHandler.removeCallbacks(recorderRunnable);
}
}
/**
* To define the number of frame you want to skip
* There is a frame each 16ms to reach 60 fps
* @param skipFrames
*/
public void setSkipFrames(int skipFrames) {
this.skipFrames = skipFrames;
}
/**
* Define the name of the file on the disk you have to use the tag attribute
* @return The name of the file on the disk
*/
private String getName() {
if (mName == null) {
if (getTag() != null && getTag() instanceof String) {
mName = (String) getTag() ;
} else {
mName = "NoTagSet" ;
}
}
return mName;
}


//There is 60 frames by second
//so skip frames keeping that in mind
/** Number of frames to skip */
int skipFrames = 2;
/**Number of skipped frames */
int skippedFrames = 0;
/**Number of recorded screen capture*/
int recordedFrameNum = 0;
/** The name of the screen capture on the disk */
String mName = null;
/** Use to track the time of some operations*/
long debut,fin;
@Override
public void draw(Canvas canvas) {
if(canvas!=null){
super.draw(canvas);
}
if(screenCaptureOn) {
skippedFrames++;
if (skippedFrames == skipFrames) {
// debut= System.currentTimeMillis();
// Log.e("LinearLayoutScreenRec", "onDraw screenCaptureOn skippedFrames" + skippedFrames);
mCurrentBitmap = Bitmap.createBitmap(layoutW, layoutH, Bitmap.Config.ARGB_8888);
mBitmapCanvas = new Canvas(mCurrentBitmap);
mBitmapCanvas.drawRect(0, 0, layoutW, layoutH, paint);
super.draw(mBitmapCanvas);
recordedFrameNum++;
new Thread(new saveOnDiskRunnable(mCurrentBitmap, recordedFrameNum)).start();
// fin = System.currentTimeMillis();
//using this method take -1091milli
// Log.e("LinearLayoutScreenRec", "saveBitmap finished in " + (debut - fin));
skippedFrames = 0;
}
}
}

/***********************************************************
* The Handler and the Runnable to launch a screen capture
**********************************************************/
/**
* The Handler to run the recorderRunnable in the UI thread
*/
Handler recorderHandler =new Handler();
/**
* The Runnbale to record the screen
*/
Runnable recorderRunnable =new Runnable() {
@Override
public void run() {
//call draw
draw(null);
//call yourself in 16ms
recorderHandler.postDelayed(this, 16);
}
};

/***********************************************************
* Saving the Bitmap as a file on the disk
**********************************************************/
/**
* The runnnable that saves a Bitmap on the disk as a file
*/
public class saveOnDiskRunnable implements Runnable{

/**The bitmap to save*/
Bitmap currentBitmap;
/**The number of the bitmap to define the file's name*/
int frameNum;

/**
* Constructor
* @param currentBitmap The bitmap to save
* @param frameNum The number of the bitmap to define the file's name
*/
public saveOnDiskRunnable(Bitmap currentBitmap, int frameNum) {
this.currentBitmap = currentBitmap;
this.frameNum = frameNum;
}

@Override
public void run() {
//so save the bitmap
saveBitmap(currentBitmap,frameNum);
}
}

/**
* Saving a bitmap
* @param currentBitmap The bitmap to save
* @param frameNum The number of the bitmap to define the file's name
*/
private void saveBitmap(Bitmap currentBitmap,int frameNum) {
if (mRootFolder == null) {
//Save the picture
//--------------------------
//Find the external storage directory
File filesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
//Retrieve the name of the subfolder where your store your picture
//(You have set it in your string ressources)
String pictureFolderName = getContext().getString(R.string.app_name);
//then create the subfolder
mRootFolder = new File(filesDir, pictureFolderName);
//Check if this subfolder exists
if (!mRootFolder.exists()) {
//if it doesn't create it
mRootFolder.mkdirs();
}
//tell to the media scanner to had this folder in its scan
//(can be optimized...)
MediaScannerConnection.scanFile(getContext(), new String[]{mRootFolder.getAbsolutePath()}, null, new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
Log.d("LinearLayoutScreenRec", "Scan done for the path : " + path);

}
});
}
try {
//Define the file to store your picture in
File filePicture = new File(mRootFolder, getName()+frameNum+ ".png");//TODO +frameNumber
filePicture.setWritable(true);
//Open an OutputStream on that file
FileOutputStream fos = new FileOutputStream(filePicture);
//Write in that stream your bitmap in png with the max quality (100 is max, 0 is min quality)
currentBitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
//The close properly your stream
fos.flush();
fos.close();
//then recycle the bitmap
currentBitmap.recycle();
//and again tell the mediascanner to add this file
MediaScannerConnection.scanFile(getContext(), new String[]{filePicture.getAbsolutePath()}, null, new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
Log.d("LinearLayoutScreenRec", "Scan done for the path : " + path);
}
});
Log.d("LinearLayoutScreenRec", "The file has been recorded as " + filePicture.getName() + " in " + mRootFolder);

} catch (FileNotFoundException e) {
//Ok, I should have managed my exception, but this only for debug
Log.e("LinearLayoutScreenRec", "F*ck occurs FileNotFoundException", e);
} catch (IOException e) {
//Ok, I should have managed my exception, but this only for debug
Log.e("LinearLayoutScreenRec", "F*ck occurs IOException", e);
}
}
}

 Bon, ben y'a plus qu'à l'utiliser alors:

Il vous suffit de prendre le Layout que vous souhaitez enregistrer et de changer son LinearLayout par:


<com.android2ee.droidcon.greece.animation.customviews.LinearLayoutScreenRecorder
android:id="@+id/llDrawableScreenRecorder"
android:tag="DrawableScreenRecorder"
....
>

 Et dans votre activité (ou votre fragment):

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_drawable);
llScreenRecorder= (LinearLayoutScreenRecorder) findViewById(R.id.llDrawableScreenRecorder);
@Override
protected void onResume() {
super.onResume();
llScreenRecorder.startRecording();
}
@Override
protected void onPause() {
super.onPause();
llScreenRecorder.stopRecording();
}

et ça record :)

 DrawableScreenRecorder3 DrawableScreenRecorder5 DrawableScreenRecorder7
 DrawableScreenRecorder9  DrawableScreenRecorder16  DrawableScreenRecorder21
   DrawableScreenRecorder26  

 

Les prochaines formations Android d’Android2EE 

Toulouse : 14-18 Septembre 2015, Formation complète

Paris: 21-25 Septembre 2015, Formation complète.

Et pour le reste, je passerai régulièrement sur Paris, Toulouse, Lyon avec la formation complète : regardez le calendrier il est à jour :) Et puis je vais placer des formations Master et Utlimate, je sais pas encore où ni quand, même c'est prévu.

A bientôt.

Mathias Séguy
Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser.

Fondateur Android2EE
Formation – Expertise – Consulting Android.
Ebooks pour apprendre la programmation sous Android.
AndroidEvolution

Suivez moi sur Twitter
Rejoignez mon réseau LinkedIn

 

 

Related Posts