Friday, October 6, 2017

Understanding Subtle Animations in PLAID app - Part-1

Toolbar title animation

Android developer options have great support when it comes to understanding the animations in app.
Please take a look below video and set animation scale to 5x




Now lets come to the point and see how toolbar title animates.


As you must have seen in the above video , the toolbar title animates and its a subtle but pretty nice and easy on eyes , soothing animation. 

Now lets see what are observations before we look at implementation to see how one would go about implementing this.

Observations:
  • Initially the title was not visibile -  [ Alpha must be zero ] 
  • Animation starts after some delay - [ There must be a start delay ] 
  • See the letter spacing effect - [ At present not sure - how its achieved ] 
  • It appears to scale horizontally but not from zero - [ There must be some initial value scaleX ] 
  • Animation starts fast and slows as the time progresses - [ Must be some kind of Interpolator at play ] 

Lets look at implementation from the Plaid App.
There is an aptly named animateToolbar() method in the HomeActivity.java.

private void animateToolbar() {
        // this is gross but toolbar doesn't expose it's children to animate them :(
        View t = toolbar.getChildAt(0);
        if (t != null && t instanceof TextView) {
            TextView title = (TextView) t;

            // fade in and space out the title.  Animating the letterSpacing performs horribly so
            // fake it by setting the desired letterSpacing then animating the scaleX ¯\_(ツ)_/¯
            title.setAlpha(0f);
            title.setScaleX(0.8f);

            title.animate()
                    .alpha(1f)
                    .scaleX(1f)
                    .setStartDelay(300)
                    .setDuration(900)
                    .setInterpolator(AnimUtils.getFastOutSlowInInterpolator(this));
        }
    }

Again these are the kind of animations that make our apps a treat to use for consumers.
One last point before we end this post.  This animation is called only when we are launching this activity a fresh for the first time. This is again an important detail so that we don't overdo the animation.

 if (savedInstanceState == null) {
            animateToolbar();
    }

Thats it for this post and there will be more post in this series. So please look forward to it and do let me know in the comments if you like the content.

Thursday, September 15, 2016

Storage in Android and how to make sense of it

Storage in Android

Methods on Context 
Methods on Environment
  • Methods on Context 
    • Context.getFilesDir()       
    • Context.getCacheDir()
               These methods store the files in primary storage (internal SD card) which is protected
               This cannot be seen on an unrooted device when USB is connected.
               Files stored here are automatically removed when app is uninstalled.
               App doesn't require any specific permissions to read / write from this location.
    • Context.getExternalFilesDir()
    • Context.getExternalCacheDir()
               These methods store the files in primary storage (internal SD card) which is un-protected
               This can be seen on an unrooted device when USB is connected.
               Files stored here are automatically removed when app is uninstalled.
               App doesn't require any specific permissions to read / write from this location.

  • Methods on Environment
    • Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS ,FILENAME);
      • Environment.DIRECTORY_DOCUMENTS
      •  Environment.DIRECTORY_DOWNLOADS;
      •  Environment.DIRECTORY_MOVIES;
      •  Environment.DIRECTORY_MUSIC;
      •  Environment.DIRECTORY_NOTIFICATIONS;
      •  Environment.DIRECTORY_PICTURES;
      •  Environment.DIRECTORY_PODCASTS;
      •  Environment.DIRECTORY_RINGTONES; 

    • Environment.getExternalStorageDirectory()
                    Apps shouldn't really be using this location to store app specific data as it points to root 
                    of the storage unlike the specific directories like "Music / Pictures / Podcasts " as was the
                    case with previous call.

                    Files stored here are not automatically removed when app is uninstalled.

                    Writing to this location requires you to have WRITE_EXTERNAL_STORAGE and 
                    starting in kitkat reading requires READ_EXTERNAL_STORAGE which is
                    automatically granted if you have write permission
                     
                        











Thursday, February 4, 2016

Understanding Widgets in Plaid App - Part 1

CutoutTextView.java

Launch Plaid app  and  Go to Menu -> About
You will be able to see the text "PLAID"  but look close you can actually see through it.
Today we will go into details of how this is implemented.



We can see that there is textview PLAID and we can actually see through it . After seeing that Portar Duff should be in your mind.

There is already a comment in the source code.
"A view which punches out some text from an opaque color block, allowing you to see through it."

Really important post before you do read further.

Lets see how this view is used in layout

1
2
3
4
5
6
<io.plaidapp.ui.widget.CutoutTextView
            android:layout_width="match_parent"
            android:layout_height="@dimen/about_header_height"
            android:text="@string/app_name"
            app:foregroundColor="?android:colorPrimary"
            app:font="roboto-mono-regular" />



Custom stylable attributes can be found in file   attrs_cutout_text_view.xml


1
2
3
4
5
6
7
<resources>
    <declare-styleable name="CutoutTextView">
        <attr name="foregroundColor" format="color" />
        <attr name="android:text" />
        <attr name="font" />
    </declare-styleable>
</resources>


Constructor is really self explanatory , where we just get the staylable attributes from the xml


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 public CutoutTextView(Context context, AttributeSet attrs) {
        super(context, attrs);

        textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);

        final TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable
                .CutoutTextView, 0, 0);
        if (a.hasValue(R.styleable.CutoutTextView_font)) {
            textPaint.setTypeface(FontUtil.get(context, a.getString(R.styleable
                    .CutoutTextView_font)));
        }
        if (a.hasValue(R.styleable.CutoutTextView_foregroundColor)) {
            foregroundColor = a.getColor(R.styleable.CutoutTextView_foregroundColor,
                    foregroundColor);
        }
        if (a.hasValue(R.styleable.CutoutTextView_android_text)) {
            text = a.getString(R.styleable.CutoutTextView_android_text);
        }
        maxTextSize = context.getResources().getDimensionPixelSize(R.dimen.display_4_text_size);
        a.recycle();
    }

The main call to onSizeChanged() is done after the construction of your view but before the drawing. At this time the system will calculate the size of your view and notify you by calling onSizeChanged()


CalculateTextPosition : finds out the x,y co-ordinates within view so that the text is centered.
createBitmap : creates a canvas -> draws over it using the textPaint.

But while doing that it just punches a hole on the canvas using the Portar Duff 

// this is the magic – Clear mode punches out the bitmap
        textPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        cutoutCanvas.drawText(text, textX, textY, textPaint);


The overall effect is we are able to see through the text and we are able to see some content of the main recyclerview item that it in that position.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        calculateTextPosition();
        createBitmap();
    }

    private void calculateTextPosition() {
        float targetWidth = getWidth() / PHI;
        textSize = ViewUtils.getSingleLineTextSize(text, textPaint, targetWidth, 0f, maxTextSize,
                0.5f, getResources().getDisplayMetrics());
        textPaint.setTextSize(textSize);

        // measuring text is fun :] see: https://chris.banes.me/2014/03/27/measuring-text/
        textX = (getWidth() - textPaint.measureText(text)) / 2;
        Rect textBounds = new Rect();
        textPaint.getTextBounds(text, 0, text.length(), textBounds);
        float textHeight = textBounds.height();
        textY = (getHeight() + textHeight) / 2;
    }

    private void createBitmap() {
        if (cutout != null && !cutout.isRecycled()) {
            cutout.recycle();
        }
        cutout = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        cutout.setHasAlpha(true);
        Canvas cutoutCanvas = new Canvas(cutout);
        cutoutCanvas.drawColor(foregroundColor);

        // this is the magic – Clear mode punches out the bitmap
        textPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        cutoutCanvas.drawText(text, textX, textY, textPaint);
    }

Tuesday, January 26, 2016

Understanding Architecture of Nick Butcher's Plaid App - Part-6

AboutActivity.java




About screen. This displays 3 pages in a ViewPager:
  – About Plaid
  – Credit Roman for the awesome icon
  – Credit libraries


ViewPager associates each page with a key Object instead of working with Views directly. This key is used to track and uniquely identify a given page independent of its position in the adapter

A very simple PagerAdapter may choose to use the page Views themselves as key objects, returning them from instantiateItem(ViewGroup, int) after creation and adding them to the parent ViewGroup. A matching destroyItem(ViewGroup, int, Object) implementation would remove the View from the parent ViewGroup and isViewFromObject(View, Object) could be implemented as return view == object;.


21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
  static class AboutPagerAdapter extends PagerAdapter {

        private View aboutPlaid;
        @Nullable @Bind(R.id.about_description) TextView plaidDescription;
        private View aboutIcon;
        @Nullable @Bind(R.id.icon_description) TextView iconDescription;
        private View aboutLibs;
        @Nullable @Bind(R.id.libs_list) RecyclerView libsList;

        private final LayoutInflater layoutInflater;
        private final Bypass markdown;

        public AboutPagerAdapter(Context context) {
            layoutInflater = LayoutInflater.from(context);
            markdown = new Bypass(context, new Bypass.Options());
        }

        @Override
        public Object instantiateItem(ViewGroup collection, int position) {
            View layout = getPage(position, collection);
            collection.addView(layout);
            return layout;
        }

        @Override
        public void destroyItem(ViewGroup collection, int position, Object view) {
            collection.removeView((View) view);
        }

        @Override
        public int getCount() {
            return 3;
        }

        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }

        private View getPage(int position, ViewGroup parent) {
            switch (position) {
                case 0:
                    if (aboutPlaid == null) {
                        aboutPlaid = layoutInflater.inflate(R.layout.about_plaid, parent, false);
                          :
                        return aboutPlaid;
                case 1:
                    if (aboutIcon == null) {
                        aboutIcon = layoutInflater.inflate(R.layout.about_icon, parent, false);
                        ButterKnife.bind(this, aboutIcon);
                        CharSequence icon0 = parent.getResources().getString(R.string.about_icon_0);
                        CharSequence icon1 = markdown.markdownToSpannable(parent.getResources()
                                .getString(R.string.about_icon_1), iconDescription, null);
                        CharSequence iconDesc = TextUtils.concat(icon0, "\n", icon1);
                        HtmlUtils.setTextWithNiceLinks(iconDescription, iconDesc);
                    }
                    return aboutIcon;
                case 2:
                    if (aboutLibs == null) {
                        aboutLibs = layoutInflater.inflate(R.layout.about_libs, parent, false);
                        ButterKnife.bind(this, aboutLibs);
                        libsList.setAdapter(new LibraryAdapter(parent.getContext()));
                    }
                    return aboutLibs;
            }
            throw new InvalidParameterException();
        }
    }


You will also find a LibraryAdapter which is just a simple recyclerview adapter for recyclerview on the third page which shows popular android opensource libraries.

Since we are doing the Architecture explanation here , we will not go into details of how the elastic effect is achieved and how awesomely that InkPageIndicator is implemented.

That info is for the Learnings from Plaid series which will definitely cover in that series :-) till that time just appreciate the effect.



Understanding Architecture of Nick Butcher's Plaid App - Part-5

Today we are going to look at how Plaid app communicates with Dribble API and that its the same mechanism with which it communicates with Designer News and Producthunt APIs. but for the purpose of this blog we are going to look at only the Dribble APIs.

This is the tree that we will discuss today.

Data->api
  • Designernews
  • Dribble
  • Producthunt     







If you open the navigation drawer and click on
  • Dribble following
  • My Dribble Shots
  • My Dribble likes

You will be asked for a login into Dribble.


Plaid app uses Retrofit to for the communication with Dribble API , why ? Just because its awesome , you will see how in this post.  Retrofit is popular library among all android developers for communicating with Rest APIs and its from SQUARE.

Model Classes : 

Shot.java - modeled from http://developer.dribbble.com/v1/shots/
User.java  - modeled from http://developer.dribbble.com/v1/users/

Response
The response will be returned as JSON and takes the following form:
{
 
"access_token" : "29ed478ab86c07f1c069b1af76088f7431396b7c4a2523d06911345da82224a0",
 
"token_type" : "bearer",
 
"scope" : "public write"
}

Images.java - modeled from field images from  http://developer.dribbble.com/v1/shots/


Retrofit endpoint specific implementation

DribbbleAuthService.java
      This service helps authenticating with Dribble API

 DribbbleService.java
     This service helps fetch the data from Dribble API once the authentication is successful



DribbleLogin.java 

Flow :   Once you press on login button

DribbleLogin.java
  public void doLogin(View view) {
        showLoading();
        dribbblePrefs.login(DribbbleLogin.this);
    }


DribblePrefs.java
 public static final String LOGIN_URL = "https://dribbble.com/oauth/authorize?client_id="
            + BuildConfig.DRIBBBLE_CLIENT_ID
            + "&redirect_uri=plaid%3A%2F%2F" + LOGIN_CALLBACK
            + "&scope=public+write+comment+upload";


 public void login(Context context) {
        Log.d("#### " , Uri.parse(LOGIN_URL).toString());
        context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(LOGIN_URL)));
    }


for Dribblelogin activity , in AndroidManifest.xml, we have  android:launchmode = "SingleTop" ,
Once we are redirected back to the activity after authentication , we use the same activity instance and redeliver the intent to same activity.

This time onNewIntent() is invoked and since we will receive the access token as part of this intent we fetch the accesstoken.  Then we authenticate with Dribble auth endpoint using the client ID / SECRET and the access token that we received. Once the authentication is successful , the logged in user is displayed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
   @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        checkAuthCallback(intent);
    }

  
private void checkAuthCallback(Intent intent) {
        if (intent != null
                && intent.getData() != null
                && !TextUtils.isEmpty(intent.getData().getAuthority())
                && DribbblePrefs.LOGIN_CALLBACK.equals(intent.getData().getAuthority())) {
            showLoading();
            getAccessToken(intent.getData().getQueryParameter("code"));
        }

    }



  private void getAccessToken(String code) {
        RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(DribbbleAuthService.ENDPOINT)
                .build();

        DribbbleAuthService dribbbleAuthApi = restAdapter.create((DribbbleAuthService.class));

        dribbbleAuthApi.getAccessToken(BuildConfig.DRIBBBLE_CLIENT_ID,
                BuildConfig.DRIBBBLE_CLIENT_SECRET,
                code, "", new Callback<AccessToken>() {
                    @Override
                    public void success(AccessToken accessToken, Response response) {
                        dribbblePrefs.setAccessToken(accessToken.access_token);
                        showLoggedInUser();
                        setResult(Activity.RESULT_OK);
                        finishAfterTransition();
                    }

                    @Override
                    public void failure(RetrofitError error) {
                        Log.e(getClass().getCanonicalName(), error.getMessage(), error);
                        // TODO snackbar?
                        Toast.makeText(getApplicationContext(), "Log in failed: " + error
                                .getResponse()
                                .getStatus(), Toast.LENGTH_LONG).show();
                        showLogin();
                    }
                });
    }


private void showLoggedInUser() {
        Gson gson = new GsonBuilder()
                .setDateFormat(DribbbleService.DATE_FORMAT)
                .create();

        RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(DribbbleService.ENDPOINT)
                .setConverter(new GsonConverter(gson))
                .setRequestInterceptor(new AuthInterceptor(dribbblePrefs.getAccessToken()))
                .build();

        DribbbleService dribbbleApi = restAdapter.create((DribbbleService.class));
        dribbbleApi.getAuthenticatedUser(new Callback<User>() {
            @Override
            public void success(User user, Response response) {
                dribbblePrefs.setLoggedInUser(user);
                Toast confirmLogin = new Toast(getApplicationContext());
                View v = LayoutInflater.from(DribbbleLogin.this).inflate(R.layout
                        .toast_logged_in_confirmation, null, false);
                ((TextView) v.findViewById(R.id.name)).setText(user.name);
                // need to use app context here as the activity will be destroyed shortly
                Glide.with(getApplicationContext())
                        .load(user.avatar_url)
                        .placeholder(R.drawable.ic_player)
                        .transform(new CircleTransform(getApplicationContext()))
                        .into((ImageView) v.findViewById(R.id.avatar));
                v.findViewById(R.id.scrim).setBackground(ScrimUtil.makeCubicGradientScrimDrawable
                        (ContextCompat.getColor(DribbbleLogin.this, R.color.scrim),
                                5, Gravity.BOTTOM));
                confirmLogin.setView(v);
                confirmLogin.setGravity(Gravity.BOTTOM | Gravity.FILL_HORIZONTAL, 0, 0);
                confirmLogin.setDuration(Toast.LENGTH_LONG);
                confirmLogin.show();
            }

            @Override
            public void failure(RetrofitError error) {
            }
        });
    }

AuthInterceptor.java 

A {@see RequestInterceptor} that adds an auth token to requests

 @Override
    public void intercept(RequestFacade request) {
        request.addHeader("Authorization", "Bearer " + accessToken);
    }


Use the access token to access the API.
The access token allows you to make requests to the API on a behalf of a user.
You can pass the token in the query parameters like shown above, but a cleaner approach is to include it in the Authorization header:
Authorization: Bearer ACCESS_TOKEN
For example, in curl you can set the Authorization header like this:
curl -H "Authorization: Bearer ACCESS_TOKEN" https://api.dribbble.com/v1/user


Saturday, January 23, 2016

Understanding Architecture of Nick Butcher's Plaid App - Part-4

FeedAdapter - FeedAdapter.java


Adapter for the main screen grid of items
  • holds on to an activity ref for the shared element transitions
  • It’s really just like any other recyclerview adapter with layout inflation in onCreateView () and binding of views to viewholders in onBindView()  
  • it deals with following items  : - private List<PlaidItem> items;
  • It deals with four different itemview types  
    •  private static final int TYPE_DESIGNER_NEWS_STORY = 0;
    •  private static final int TYPE_DRIBBBLE_SHOT = 1;
    •  private static final int TYPE_PRODUCT_HUNT_POST = 2;
    •  private static final int TYPE_LOADING_MORE = -1;  
This adapter initialized is initialized in HomeActivity.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
 @Bind(R.id.stories_grid) RecyclerView grid;

     private FeedAdapter adapter;


adapter = new FeedAdapter(this, dataManager, columns, PocketUtils.isPocketInstalled(this));

        grid.setAdapter(adapter);

        layoutManager = new GridLayoutManager(this, columns);

        layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {

            @Override

            public int getSpanSize(int position) {

                return adapter.getItemColumnSpan(position);

            }

        });


        grid.setLayoutManager(layoutManager);

        grid.addOnScrollListener(gridScroll);

        grid.addOnScrollListener(new InfiniteScrollListener(layoutManager, dataManager) {

            @Override

            public void onLoadMore() {

                dataManager.loadAllDataSources();

            }

        });

        grid.setHasFixedSize(true);

        grid.addItemDecoration(new GridItemDividerDecoration(adapter.getDividedViewHolderClasses(),

                this, R.dimen.divider_height, R.color.divider));

        grid.setItemAnimator(new HomeGridItemAnimator());


Its important to note that the  TYPE_LOADING_MORE is returned when the scrolled position is more than the items in  the List<PlaidItems>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  @Override
    public int getItemViewType(int position) {
        if (position < getDataItemCount()
                && getDataItemCount() > 0) {
            PlaidItem item = getItem(position);
            if (item instanceof Story) {
                return TYPE_DESIGNER_NEWS_STORY;
            } else if (item instanceof Shot) {
                return TYPE_DRIBBBLE_SHOT;
            } else if (item instanceof Post) {
                return TYPE_PRODUCT_HUNT_POST;
            }
        }
        return TYPE_LOADING_MORE;
    }