I developed a custom animation for my Splash Screen activity :
=> Here is an animation that show what is happening :

Of course my real app :
My designer provided me 60 png files.
=> One example to illustrate :

My target is:
To run this, I have one Multilayer layout for the SpashScreenActivity with:
Here is the xml code for the SpashScreen Layout :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"
android:orientation="vertical">
<FrameLayout
android:id="@+id/fl_logo_top_marge_hidden"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="invisible"
android:background="@color/colorPrimary" />
<include
android:visibility="invisible"
android:id="@+id/l_logo_activate_hidden"
layout="@layout/part_logo_activate"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<FrameLayout
android:visibility="invisible"
android:id="@+id/fl_logo_bottom_marge_hidden"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@color/colorPrimary" />
<fr.millezimsolutions.app.splashanimation.SquareAspectWidthBasedImageView
android:visibility="invisible"
android:id="@+id/iv_home_hidden"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitXY"
android:src="@drawable/p_log_android" />
<FrameLayout
android:visibility="invisible"
android:id="@+id/fl_bar_hidden"
android:layout_width="match_parent"
android:layout_height="@dimen/start_degustation_bar_height"
android:background="@color/colorAccent"
android:gravity="bottom" />
</LinearLayout>
<LinearLayout
android:id="@+id/fl_middle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:background="@color/colorPrimary"
android:gravity="bottom"
android:orientation="vertical">
<fr.millezimsolutions.app.splashanimation.FitXCropTopImageView
android:id="@+id/iv_slogan"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorTransparent"
android:scaleType="fitStart"
android:src="@drawable/p_log_apple" />
<FrameLayout
android:id="@+id/fl_logo_bottom_bar"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/colorAccent"
android:gravity="bottom" />
</LinearLayout>
<LinearLayout
android:id="@+id/fl_front"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/fl_logo_top_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="40"
android:background="@color/colorPrimary" />
<FrameLayout
android:id="@+id/fl_logo_top_marge"
android:layout_width="match_parent"
android:layout_height="5dp"
android:background="@color/colorTransparent" />
<include
android:id="@+id/l_logo_activate"
layout="@layout/part_logo_activate"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<FrameLayout
android:id="@+id/fl_logo_bottom_marge"
android:layout_width="match_parent"
android:layout_height="5dp"
android:background="@color/colorTransparent" />
<FrameLayout
android:id="@+id/fl_logo_bottom_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="40"
android:background="@color/colorTransparent" />
</LinearLayout>
</RelativeLayout>
2 The xml code for the top of the layout (via include)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorTransparent"
android:gravity="bottom"
android:orientation="vertical">
<FrameLayout
android:id="@+id/fl_home_marginTop"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/iv_millezimuLogo"
android:layout_width="wrap_content"
android:layout_height="64dp"
android:layout_gravity="center_horizontal"
android:layout_marginLeft="@dimen/marge"
android:layout_marginRight="@dimen/marge"
android:src="@drawable/p_log_so" />
<TextView
android:id="@+id/tv_slogan"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="@dimen/marge_small"
android:layout_marginTop="@dimen/marge_small_border"
android:gravity="center"
android:hint=""
android:text="Bonjour"
android:textColor="@color/colorAccent"
android:textSize="20sp" />
<ImageView
android:id="@+id/iv_sponsorLogo"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:layout_marginLeft="@dimen/marge"
android:layout_marginRight="@dimen/marge"
android:src="@drawable/p_log_so"
android:visibility="gone" />
<TextView
android:id="@+id/tv_sponsorLogo"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:layout_marginLeft="@dimen/marge"
android:layout_marginRight="@dimen/marge"
android:gravity="center"
android:textColor="@color/colorAccent"
android:textSize="20sp"
android:visibility="gone" />
<FrameLayout
android:id="@+id/fl_home_marginBottom"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
3 Here is the code of the Activity.
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
public class SplashScreenActivity extends AppCompatActivity {
// Splash screen timer
private static int SPLASH_TIME_OUT = 3000;
private int finalTopMarge, finalBottomMarge, topMargeDec, bottomMargeInc;
long currentTimeStamp;
private int[] mSplashAnimFrames = {R.drawable.p_wave_spashscreen_00, R.drawable.p_wave_spashscreen_01, R.drawable.p_wave_spashscreen_02, R.drawable.p_wave_spashscreen_03, R.drawable.p_wave_spashscreen_04, R.drawable.p_wave_spashscreen_05, R.drawable.p_wave_spashscreen_06, R.drawable.p_wave_spashscreen_07, R.drawable.p_wave_spashscreen_08, R.drawable.p_wave_spashscreen_09,
R.drawable.p_wave_spashscreen_10, R.drawable.p_wave_spashscreen_11, R.drawable.p_wave_spashscreen_12, R.drawable.p_wave_spashscreen_13, R.drawable.p_wave_spashscreen_14, R.drawable.p_wave_spashscreen_15, R.drawable.p_wave_spashscreen_16, R.drawable.p_wave_spashscreen_17, R.drawable.p_wave_spashscreen_18, R.drawable.p_wave_spashscreen_19,
R.drawable.p_wave_spashscreen_20, R.drawable.p_wave_spashscreen_21, R.drawable.p_wave_spashscreen_22, R.drawable.p_wave_spashscreen_23, R.drawable.p_wave_spashscreen_24, R.drawable.p_wave_spashscreen_25, R.drawable.p_wave_spashscreen_26, R.drawable.p_wave_spashscreen_27, R.drawable.p_wave_spashscreen_28, R.drawable.p_wave_spashscreen_29,
R.drawable.p_wave_spashscreen_30, R.drawable.p_wave_spashscreen_31, R.drawable.p_wave_spashscreen_32, R.drawable.p_wave_spashscreen_33, R.drawable.p_wave_spashscreen_34, R.drawable.p_wave_spashscreen_35, R.drawable.p_wave_spashscreen_36, R.drawable.p_wave_spashscreen_37, R.drawable.p_wave_spashscreen_38, R.drawable.p_wave_spashscreen_39,
R.drawable.p_wave_spashscreen_40, R.drawable.p_wave_spashscreen_41, R.drawable.p_wave_spashscreen_42, R.drawable.p_wave_spashscreen_43, R.drawable.p_wave_spashscreen_44, R.drawable.p_wave_spashscreen_45, R.drawable.p_wave_spashscreen_46, R.drawable.p_wave_spashscreen_47, R.drawable.p_wave_spashscreen_48, R.drawable.p_wave_spashscreen_49,
R.drawable.p_wave_spashscreen_50, R.drawable.p_wave_spashscreen_51, R.drawable.p_wave_spashscreen_52, R.drawable.p_wave_spashscreen_53, R.drawable.p_wave_spashscreen_54, R.drawable.p_wave_spashscreen_55, R.drawable.p_wave_spashscreen_56, R.drawable.p_wave_spashscreen_57, R.drawable.p_wave_spashscreen_58, R.drawable.p_wave_spashscreen_59};
private final int C_STOP = 120, C_MOVE = 40, C_BAR = 80;
private int bottomBarRatio;
private ImageView finalImageView;
private int targetWidth, targetHeight;
private Rect mImageViewRect;
private Paint paint;
private Bitmap original;
private Bitmap result;
private boolean setupOk = false;
private ImageView mImageView;
private Bitmap mask;
private FrameLayout ltm;
private FrameLayout lbm;
private FrameLayout lbb;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash_screen);
// Indique que l'ecran est full Screen
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
ImageManager.create(this);
}
@Override
protected void onResume() {
super.onResume();
int delay = SPLASH_TIME_OUT / C_STOP;
bottomBarRatio = getResources().getDimensionPixelSize(R.dimen.bar_nav_height) / (C_STOP - C_BAR);
runCycle(0, delay);
}
private void logStamp() {
long oldTimeStamp = currentTimeStamp;
currentTimeStamp = System.currentTimeMillis();
long delay = currentTimeStamp - oldTimeStamp;
Log.v("TIMESTAMP", String.valueOf(delay));
}
public void runCycle(final int cycle, final int delay) {
if (BuildConfig.DEBUG)
logStamp();
Handler cyclic = new Handler();
cyclic.postDelayed(new Runnable() {
@Override
public void run() {
if (cycle >= C_STOP) {
closeActivity();
} else {
runCycle(cycle + 1, delay);
if (cycle >= C_MOVE) {
// Copy des hauteurs pour les marges
initFinalLogoMargeHeight();
// Decroissance du poid de layout superieur
MoveUpLogo();
// bouger la bar
if (cycle >= C_BAR) {
updateBottomBar(cycle - C_BAR);
}
findViewById(R.id.fl_front).requestLayout();
}
if (setupFinalView()) {
if ((cycle % 2) == 0)
updateImageViewLight(cycle / 2);
}
}
}
}, delay);
}
private boolean setupFinalView() {
if (!setupOk) {
finalImageView = (ImageView) findViewById(R.id.iv_home_hidden);
targetWidth = finalImageView.getWidth();
targetHeight = finalImageView.getHeight();
mImageViewRect = new Rect(0, 0, finalImageView.getWidth(), finalImageView.getHeight());
mImageView = (ImageView) findViewById(R.id.iv_slogan);
mImageView.setBackgroundResource(R.drawable.p_log_apple);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
ltm = (FrameLayout) findViewById(R.id.fl_logo_top_marge);
lbm = (FrameLayout) findViewById(R.id.fl_logo_bottom_marge);
lbb = ((FrameLayout) findViewById(R.id.fl_logo_bottom_bar));
if (targetWidth > 0 && targetHeight > 0) {
original = ImageManager.decodeSampledBitmapFromResource(getResources(), R.drawable.p_log_android, targetWidth, targetHeight);
result = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_4444);
setupOk = true;
}
}
return setupOk;
}
private void MoveUpLogo() {
ViewGroup.LayoutParams ltmp = ltm.getLayoutParams();
ltmp.height -= topMargeDec;
ViewGroup.LayoutParams lbmp = lbm.getLayoutParams();
lbmp.height += bottomMargeInc;
}
private void initFinalLogoMargeHeight() {
if (finalBottomMarge == 0) {
finalTopMarge = findViewById(R.id.fl_logo_top_marge_hidden).getHeight();
topMargeDec = (findViewById(R.id.fl_logo_top_marge).getHeight() - finalTopMarge) / C_BAR;
finalBottomMarge = findViewById(R.id.fl_logo_bottom_marge_hidden).getHeight() + findViewById(R.id.fl_bar_hidden).getHeight() + findViewById(R.id.iv_home_hidden).getHeight();
bottomMargeInc = (finalBottomMarge - findViewById(R.id.fl_logo_bottom_marge).getHeight()) / C_BAR;
}
}
private void updateBottomBar(int cycle) {
LinearLayout.LayoutParams lbbp = (LinearLayout.LayoutParams) lbb.getLayoutParams();
lbbp.height = cycle * bottomBarRatio;
lbb.setLayoutParams(lbbp);
}
private void closeActivity() {
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
Intent i = new Intent(SplashScreenActivity.this, MainActivity.class);
startActivity(i);
finish();
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
}
private int getNext(int index) {
if (index < (mSplashAnimFrames.length - 1))
index++;
else
index = mSplashAnimFrames.length - 1;
return mSplashAnimFrames[index];
}
public void updateImageViewLight(int index) {
mask = ImageManager.decodeSampledBitmapFromResource(getResources(), getNext(index), targetWidth, targetHeight);
Canvas mCanvas = new Canvas(result);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
mCanvas.drawBitmap(original, null, mImageViewRect, null);
mCanvas.drawBitmap(mask, null, mImageViewRect, paint);
paint.setXfermode(null);
mImageView.setImageBitmap(result);
}
}
4 And the code of the ImageManager for understanding (I use UIL)
public class ImageManager {
private static Context context;
public static ImageLoader getImageLoader() {
return ImageLoader.getInstance();
}
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.outWidth = reqWidth;
options.outHeight = reqHeight;
options.inJustDecodeBounds = true;
options.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return getResourceImageForCanvas(resId, new ImageSize(reqWidth, reqHeight));
}
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
public static Bitmap getResourceImageForCanvas(int bitmapResourceId, ImageSize targetImageSize) {
DisplayImageOptions options = new DisplayImageOptions.Builder().bitmapConfig(Bitmap.Config.RGB_565).build();
return getImageLoader().loadImageSync("drawable://" + bitmapResourceId, targetImageSize, options);
//
}
public static void create(Context context) {
try {
ImageManager.context = context;
initImageLoader();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void initImageLoader() throws IOException {
// Create global configuration and initialize ImageLoader with this
// configuration
BitmapFactory.Options opt = new BitmapFactory.Options();
// opt.inScaled = false;
opt.inSampleSize = 1;
opt.inDither = true;
opt.inPreferredConfig = Bitmap.Config.RGB_565;
opt.inPreferQualityOverSpeed = false;
DisplayImageOptions defaultOptions = new DisplayImageOptions.Builder()./* cacheInMemory(true). */cacheOnDisk(true).decodingOptions(opt).imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2)
.bitmapConfig(Bitmap.Config.RGB_565).build();
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context).defaultDisplayImageOptions(defaultOptions).memoryCacheSizePercentage(13).writeDebugLogs().build();
ImageLoader.getInstance().init(config);
}
}
STATUS:
The updateImageViewLight method may provably help other to deal with this kind of behavior (PortedDuff...) that wasn't easy to find.
The animation works fine on a powerful device, but often lag if the device or the app does something else.
I have tried to run this calculation in Async Task but it was less powerful than in the mainThread
QUESTIONS:
I'm looking for any educated advise on my implementation, which may help to improve :
But also :
Not yet fully answer, but it is a try, that need to be continued and commented.
First optimisation:
ImageView extension) for the wave, that will refresh on onDraw method static final class membersonDraw (Canvas and Paint) as well as unnecessary object update Here is the code of the CustomView that extends ImageView
public class WaveFillingImageView extends ImageView {
private final static int[] mSplashAnimFrames = {R.drawable.p_wave_spashscreen_00, R.drawable.p_wave_spashscreen_01, R.drawable.p_wave_spashscreen_02, R.drawable.p_wave_spashscreen_03, R.drawable.p_wave_spashscreen_04, R.drawable.p_wave_spashscreen_05, R.drawable.p_wave_spashscreen_06, R.drawable.p_wave_spashscreen_07, R.drawable.p_wave_spashscreen_08, R.drawable.p_wave_spashscreen_09,
R.drawable.p_wave_spashscreen_10, R.drawable.p_wave_spashscreen_11, R.drawable.p_wave_spashscreen_12, R.drawable.p_wave_spashscreen_13, R.drawable.p_wave_spashscreen_14, R.drawable.p_wave_spashscreen_15, R.drawable.p_wave_spashscreen_16, R.drawable.p_wave_spashscreen_17, R.drawable.p_wave_spashscreen_18, R.drawable.p_wave_spashscreen_19,
R.drawable.p_wave_spashscreen_20, R.drawable.p_wave_spashscreen_21, R.drawable.p_wave_spashscreen_22, R.drawable.p_wave_spashscreen_23, R.drawable.p_wave_spashscreen_24, R.drawable.p_wave_spashscreen_25, R.drawable.p_wave_spashscreen_26, R.drawable.p_wave_spashscreen_27, R.drawable.p_wave_spashscreen_28, R.drawable.p_wave_spashscreen_29,
R.drawable.p_wave_spashscreen_30, R.drawable.p_wave_spashscreen_31, R.drawable.p_wave_spashscreen_32, R.drawable.p_wave_spashscreen_33, R.drawable.p_wave_spashscreen_34, R.drawable.p_wave_spashscreen_35, R.drawable.p_wave_spashscreen_36, R.drawable.p_wave_spashscreen_37, R.drawable.p_wave_spashscreen_38, R.drawable.p_wave_spashscreen_39,
R.drawable.p_wave_spashscreen_40, R.drawable.p_wave_spashscreen_41, R.drawable.p_wave_spashscreen_42, R.drawable.p_wave_spashscreen_43, R.drawable.p_wave_spashscreen_44, R.drawable.p_wave_spashscreen_45, R.drawable.p_wave_spashscreen_46, R.drawable.p_wave_spashscreen_47, R.drawable.p_wave_spashscreen_48, R.drawable.p_wave_spashscreen_49,
R.drawable.p_wave_spashscreen_50, R.drawable.p_wave_spashscreen_51, R.drawable.p_wave_spashscreen_52, R.drawable.p_wave_spashscreen_53, R.drawable.p_wave_spashscreen_54, R.drawable.p_wave_spashscreen_55, R.drawable.p_wave_spashscreen_56, R.drawable.p_wave_spashscreen_57, R.drawable.p_wave_spashscreen_58, R.drawable.p_wave_spashscreen_59};
private Paint paint;
private long nextDrawTimeStamp;
private boolean init = false, isStarted = false;
private Bitmap original, result;
private Rect mImageViewRect;
private int index = 0;
private LogAndStat las;
private Canvas mCanvas;
private final int timeTick = 50;
public WaveFillingImageView(Context context) {
super(context);
init(context);
}
public WaveFillingImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public WaveFillingImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
@DebugLog
private void init(Context context) {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
las = new LogAndStat("View", mSplashAnimFrames.length);
}
public void start() {
isStarted = true;
nextDrawTimeStamp = System.currentTimeMillis();
invalidate();
}
@DebugLog
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Before Layout
if (getWidth() == 0 || getHeight() == 0)
return;
// Init variables
if (!init) {
las.logStamp("init onDraw");
original = ImageManager.decodeSampledBitmapFromResourcewithUIL(getResources(), R.drawable.p_log_android, getWidth(), getHeight());
result = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_4444);
mCanvas = new Canvas(result);
mImageViewRect = new Rect(0, 0, getWidth(), getHeight());
init = true;
las.logStamp("init onDraw");
}
// If tick reached for refresh
if (System.currentTimeMillis() >= nextDrawTimeStamp) {
nextDrawTimeStamp += timeTick;
las.logStamp(index);
Bitmap mask = ImageManager.decodeSampledBitmapFromResourcewithUIL(getResources(), getResourceForNextCycle(index), mImageViewRect.width(), mImageViewRect.height());
mCanvas.drawBitmap(original, null, mImageViewRect, null);
mCanvas.drawBitmap(mask, null, mImageViewRect, paint);
canvas.drawBitmap(result, 0, 0, null);
index++;
}
if (isStarted)
// Invalidate during animation to call again on Draw
if (index < mSplashAnimFrames.length) {
las.logStamp("invalidate");
invalidate();
} else {
las.logStats();
}
}
@DebugLog
private int getResourceForNextCycle(int index) {
if (index < (mSplashAnimFrames.length - 1))
index++;
else
index = mSplashAnimFrames.length - 1;
return mSplashAnimFrames[index];
}
@DebugLog
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
}
Results:
onDraw method that take time at the first cycleIf you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With