A useful stack on android #3, compatibility
12 Mar 2015
This is the third post of the series: ‘A useful stack on Android’.
In the first part I have tried to define a modular and scalabe architecture, based on the pattern: Model View Presenter (MVP).
The second part has explained how to take small bites of Material Design on the user interface, through colors, transitions, vectors, etc.
In this third part, we will talk about compatibility , it is known that the Android fragmentation is huge, different versions, screen sizes, fetaures, etc.
For that reason we will decrease a few versions from the inital version of the project (Lollipop) and we will support diferent screen sizes.
All these examples are available on GitHub - https://github.com/saulmm/Material-Movies
Downgrading SDK Levels
I have choosen the 16 level of the Android SDK, from the Dashboards published by Google to date, the number of devices that support versions from Jelly Bean is an appealing 86,8 %.
To give support to these versions, some minor changes are necessary, it is the case of the transitions with shared elements, that were not introduced until the 21 version of the Android framework.
Shared element transitions
When you press a film in the MoviesActivity
, is checked if we have a version above or equal to Lollipop, if so, we may use the new API of transitions with shared elements (in this case the poster of the film); If not, I apply an animation to achieve a similar effect.
MoviesActivity
@Override
public void onClick(View v, int position,
float touchedX, float touchedY) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
startSharedElementPosition(touchedView, position,
movieDetailActivityIntent);
else
startDetailActivityAnimation(touchedView, (int) touchedX,
(int) touchedY, movieDetailActivityIntent);
}
Instead of having a shared element, which is moving between the two activities, in the MovieDetailActivity
, I will scale the poster of the film from the last point which the user has touched.
MovieDetailActivity.java
@Override
public void onCreate(Bundle savedInstanceState) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
configureEnterTransition ();
else {
mViewLastLocation = getIntent()
.getIntArrayExtra("view_location");
configureEnterAnimation ();
}
}
private void configureEnterAnimation() {
GUIUtils.startScaleAnimationFromPivot(
mViewLastLocation[0], mViewLastLocation[1],
mObservableScrollView, new AnimatorAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
GUIUtils.showViewByScale(mFabButton);
}
}
);
animateElementsByScale();
}
GuiUtils.java
public static void startScaleAnimationFromPivot (
int pivotX, int pivotY, final View v,
final AnimatorListener animatorListener) {
final AccelerateDecelerateInterpolator interpolator =
new AccelerateDecelerateInterpolator();
v.setScaleY(SCALE_START_ANCHOR);
v.setPivotX(pivotX);
v.setPivotY(pivotY);
v.getViewTreeObserver().addOnPreDrawListener(
new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
v.getViewTreeObserver().removeOnPreDrawListener(this);
ViewPropertyAnimator viewPropertyAnimator =
v.animate()
.setInterpolator(interpolator)
.scaleY(1)
.setDuration(SCALE_DELAY);
if (animatorListener != null)
viewPropertyAnimator.setListener(
animatorListener);
viewPropertyAnimator.start();
return true;
}
});
}
Result:
VectorDrawables & Slide transition
Another aspect to make compatible are the VectorDrawables
, they were not introduced until version 21 of the sdk, (LLolipop), instead, I show a little animation that scales and rotates the star, that is made by a ViewPropertyAnimator
.
The Animation: CircularReveal, is not available until LLolipop, so in the same way that with transitions, I scale a view from the last point touched.
MovieDetailActivity.java
@Override
public void showConfirmationView() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
GUIUtils.showViewByRevealEffect(mConfirmationContainer,
mFabButton, GUIUtils.getWindowWidth(this));
else
GUIUtils.startScaleAnimationFromPivot(
(int) mFabButton.getX(),(int) mFabButton.getY(),
mConfirmationContainer, null);
animateConfirmationView();
startClosingConfirmationView();
}
MovieDetailActivity.java
@Override
public void animateConfirmationView() {
Drawable drawable = mConfirmationView.getDrawable();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
if (drawable instanceof Animatable)
((Animatable) drawable).start();
else
mConfirmationView.startAnimation(
AnimationUtils.loadAnimation(this,
R.anim.appear_rotate));
}
}
Result:
Supporting different screen sizes
If something is known, android is for being able to operate in a multitude of devices, the variation between the size and quality of the different screens is very significant, to make sure everything is displayed properly in a 4” phone and a tablet of 10-12” must follow a few guides.
AutofitRecyclerView
The films are arranged in the form of grid by a RecyclerView with a GridLayoutManager. Google provides a constructor which offers a `spanCount parameter to set the number of columns.
public GridLayoutManager (Context context, int spanCount)
The problem is that depending on the width of the screen, we can need showing more columns or less.
The GridView view implements an attribute android:numColumns = "auto_fit"
to this end, unfortunately, it is not implemented in the RecyclerView
, the idea is to reuse that attribute to achieve the same objective.
Chiu-Ki Chan, already met this problem and her solution was posted in her blog. Broadly speaking it consists of setting the parameter spanCount
depending on the width of the screen.
Result:
Multiples resources
The role of resources in the android framework is undisputed, the experience that a user can have on a nexus 5 can be very different to that sits in a 10” nexus, the (MoviesDetailActivity
) has the elements differently depending on the screen that is displayed.
The optimum is to get this result with the least number of modifications both in the actual activity, MovieDetailActivity
as the layout activity_detail.xml
For this see the application resources:
We could divide notable distinctions between devices in the following points:
-
Devices with less of 600dp of screen width will use the folders without the modifier -w600dp, here you could categorize the vast majority of phones, for example: nexus 5, nexus 4, etc.
-
Devices with more of 600dp of screen width will be divided into three types: -w600dp, -w600dp-land and -w600dp-port.
In this way, we can go configuring our layouts and dimensions in different folders to change the layout according to the screen in which we find ourselves.
Also the issue that the VectorDrawable
are not available until version had been mentioned. Is not availabe until the v21 of the framework, so -v21 modifier is for that purpose.
For example, the ImageView
showing VectorDrawable
star, is different if it is a device with LLolipop or not, (<include>
).
activity_detail.xml (all versions)
<FrameLayout>
<!-- awesome hidden code -->
<include
layout="@layout/imageview_star"
/>
</FrameLayout>
imageview_star.xml (layout-v21)
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_detail_confirmation_image"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_gravity="center"
android:src="@drawable/avd_star"
/>
imageview_star.xml (layout)
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_detail_confirmation_image"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_gravity="center"
android:src="@drawable/star"
/>
I have implemented this solution, but the modularity of resources offers many possible solutions, for example, the width and height could point to a dimension in the folders *values*/dimen.xml and _ values-v21/dimen.xml_, with 150dp
and 300dp
; The drawable
could be the vector called drawable-v21/*star. xml* and in the folder drawable/*start.png* file with the image in .png.
Parallax landscape view
If you check the Google Books app, you will see that you can seea small view below the Toolbar
, when you scroll the list of books that view moves at a different speed than the Toolbar
creating an effect of Parallax
.
There are some great series called How to hide/show Toolbar when list is srolling by Michal Z. that treated this topic in deeper.
For layouts ‘-w600dp-land` and higher:
<View
android:id="@+id/activity_movies_background_view"
android:layout_width="match_parent"
android:layout_height="@dimen/
activity_movies_background_view_height"
android:background="@color/theme_primary"
/>`
This view is injected by ButterKnife, being in a nexus 5, the layout you would come from ‘activity_movies.xml’ in the folder (layouts), where that view does not exist, so we must mark the view with the annotation: ‘@Optional’.
MoviesActivity.java
@Optional
@InjectView(R.id.activity_movies_background_view)
View mTabletBackground;
MoviesActivity.java
private RecyclerView.OnScrollListener recyclerScrollListener =
new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView,
int dx, int dy) {
// awesome hidden code here
if (mTabletBackground != null) {
mBackgroundTranslation = mTabletBackground
.getY() - (dy / 2);
mTabletBackground.setTranslationY(mBackgroundTranslation);
}
}
Result: