Java tutorial
/* * Copyright (c) Joaquim Ley 2016. All Rights Reserved. * <p/> * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * <p/> * http://www.apache.org/licenses/LICENSE-2.0 * <p/> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.joaquimley.faboptions; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.PorterDuff; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.VectorDrawable; import android.os.Build; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.annotation.MenuRes; import android.support.annotation.RequiresApi; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; import android.support.v4.content.ContextCompat; import android.support.v7.view.SupportMenuInflater; import android.support.v7.view.menu.MenuBuilder; import android.support.v7.widget.AppCompatImageView; import android.transition.ChangeBounds; import android.transition.ChangeTransform; import android.transition.TransitionManager; import android.transition.TransitionSet; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import static com.joaquimley.faboptions.R.drawable.faboptions_ic_overflow; /** * FabOptions component */ @CoordinatorLayout.DefaultBehavior(FabOptionsBehavior.class) public class FabOptions extends FrameLayout implements View.OnClickListener { private static final String TAG = "FabOptions"; private static final int NO_DIMENSION = 0; private static final long CLOSE_MORPH_TRANSFORM_DURATION = 70; private boolean mIsOpen; private View.OnClickListener mListener; private Menu mMenu; // TODO: 22/11/2016 add items in runtime private FloatingActionButton mFab; private View mBackground; private View mSeparator; private FabOptionsButtonContainer mButtonContainer; public FabOptions(Context context) { this(context, null, 0); } public FabOptions(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FabOptions(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mIsOpen = false; initViews(context); setInitialFabIcon(); TypedArray fabOptionsAttributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FabOptions, 0, 0); styleComponent(context, fabOptionsAttributes); inflateButtonsFromAttrs(context, fabOptionsAttributes); } private void initViews(Context context) { inflate(context, R.layout.faboptions_layout, this); mBackground = findViewById(R.id.background); mButtonContainer = (FabOptionsButtonContainer) findViewById(R.id.button_container); mFab = (FloatingActionButton) findViewById(R.id.faboptions_fab); mFab.setOnClickListener(this); } private void setInitialFabIcon() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { VectorDrawable drawable = (VectorDrawable) getResources().getDrawable(faboptions_ic_overflow, null); mFab.setImageDrawable(drawable); } else { mFab.setImageResource(R.drawable.faboptions_ic_overflow); } } private void styleComponent(Context context, TypedArray attributes) { // If not set, the background same colour as the FAB, which if not set // it will use the default accent color int fabColor = attributes.getColor(R.styleable.FabOptions_fab_color, getThemeAccentColor(context)); int backgroundColor = attributes.getColor(R.styleable.FabOptions_background_color, fabColor); setBackgroundColor(context, backgroundColor); mFab.setBackgroundTintList(ColorStateList.valueOf(fabColor)); } public void setFabColor(@ColorRes int fabColor) { Context context = getContext(); if (context != null) { @ColorInt int colorId = ContextCompat.getColor(context, fabColor); mFab.setBackgroundTintList(ColorStateList.valueOf(colorId)); } } public void setBackgroundColor(@ColorRes int backgroundColor) { Context context = getContext(); if (context != null) { @ColorInt int color = ContextCompat.getColor(context, backgroundColor); setBackgroundColor(context, color); } else { Log.w(TAG, "Couldn't set background color, context is null"); } } private void setBackgroundColor(Context context, @ColorInt int backgroundColor) { Drawable backgroundShape = ContextCompat.getDrawable(context, R.drawable.faboptions_background); backgroundShape.setColorFilter(backgroundColor, PorterDuff.Mode.ADD); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mBackground.setBackground(backgroundShape); } else { mBackground.setBackgroundDrawable(backgroundShape); } } @ColorInt private int getThemeAccentColor(final Context context) { final TypedValue value = new TypedValue(); context.getTheme().resolveAttribute(R.attr.colorAccent, value, true); return value.data; } private void inflateButtonsFromAttrs(Context context, TypedArray attributes) { if (attributes.hasValue(R.styleable.FabOptions_button_menu)) { setButtonsMenu(context, attributes.getResourceId(R.styleable.FabOptions_button_menu, 0)); } } public boolean setButtonColor(int buttonId, @ColorRes int color) { for (int i = 0; i < mButtonContainer.getChildCount(); i++) { if (mMenu.getItem(i).getItemId() == buttonId) { return styleButton(i, color); } } Log.d(TAG, "setButtonColor(): Couldn't find button with id " + buttonId); return false; } public void setButtonsMenu(@MenuRes int menuId) { Context context = getContext(); if (context != null) { setButtonsMenu(context, menuId); } else { Log.w(TAG, "Couldn't set buttons, context is null"); } } /** * Deprecated. Use {@link #setButtonsMenu(int)} instead. */ @Deprecated public void setButtonsMenu(Context context, @MenuRes int menuId) { mMenu = new MenuBuilder(context); SupportMenuInflater menuInf = new SupportMenuInflater(context); menuInf.inflate(menuId, mMenu); addButtonsFromMenu(context, mMenu); mSeparator = mButtonContainer.addSeparator(context); animateButtons(false); } private void addButtonsFromMenu(Context context, Menu menu) { for (int i = 0; i < menu.size(); i++) { addButton(context, menu.getItem(i)); } } private void addButton(Context context, MenuItem menuItem) { AppCompatImageView button = mButtonContainer.addButton(context, menuItem.getItemId(), menuItem.getTitle(), menuItem.getIcon()); button.setOnClickListener(this); } private boolean styleButton(int buttonIndex, @ColorRes int color) { if (buttonIndex >= (mButtonContainer.getChildCount() / 2)) { // Hacky way to deal with the separator view index buttonIndex++; } if (buttonIndex >= mButtonContainer.getChildCount()) { Log.e(TAG, "Button at " + buttonIndex + " is null (index out of bounds)"); return false; } AppCompatImageView imageView = (AppCompatImageView) mButtonContainer.getChildAt(buttonIndex); imageView.setColorFilter(ContextCompat.getColor(getContext(), color)); return true; } @Override public void onClick(View v) { if (v.getId() == R.id.faboptions_fab) { if (mIsOpen) { close(); } else { open(); } } else { if (mListener != null && mIsOpen) { mListener.onClick(v); close(); } } } @Override public void setOnClickListener(View.OnClickListener listener) { mListener = listener; } private void open() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { AnimatedVectorDrawable drawable = (AnimatedVectorDrawable) getResources() .getDrawable(R.drawable.faboptions_ic_menu_animatable, null); mFab.setImageDrawable(drawable); drawable.start(); } else { mFab.setImageResource(R.drawable.faboptions_ic_close); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { TransitionManager.beginDelayedTransition(this, new OpenMorphTransition(mButtonContainer)); } animateBackground(true); animateButtons(true); // } else { // openCompatAnimation(); // } mIsOpen = true; } private void close() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { AnimatedVectorDrawable drawable = (AnimatedVectorDrawable) getResources() .getDrawable(R.drawable.faboptions_ic_close_animatable, null); mFab.setImageDrawable(drawable); drawable.start(); } else { mFab.setImageResource(R.drawable.faboptions_ic_overflow); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { TransitionManager.beginDelayedTransition(this, new CloseMorphTransition(mButtonContainer)); // } else { // closeCompatAnimation(); } animateButtons(false); animateBackground(false); mIsOpen = false; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mSeparator != null) { ViewGroup.LayoutParams separatorLayoutParams = mSeparator.getLayoutParams(); separatorLayoutParams.width = mFab.getMeasuredWidth(); separatorLayoutParams.height = mFab.getMeasuredHeight(); mSeparator.setLayoutParams(separatorLayoutParams); } } private void animateBackground(final boolean isOpen) { ViewGroup.LayoutParams backgroundLayoutParams = mBackground.getLayoutParams(); backgroundLayoutParams.width = isOpen ? mButtonContainer.getMeasuredWidth() : NO_DIMENSION; mBackground.setLayoutParams(backgroundLayoutParams); } private void openCompatAnimation() { ObjectAnimator anim = ObjectAnimator.ofFloat(mBackground, "scaleX", 1.0f); anim.setDuration(30000); // duration 3 seconds anim.start(); } private void closeCompatAnimation() { ObjectAnimator anim = ObjectAnimator.ofFloat(mBackground, "scaleX", 0.0f); anim.setDuration(3000); anim.start(); animateButtons(false); } private void animateButtons(boolean isOpen) { for (int i = 0; i < mButtonContainer.getChildCount(); i++) { mButtonContainer.getChildAt(i).setScaleX(isOpen ? 1 : 0); mButtonContainer.getChildAt(i).setScaleY(isOpen ? 1 : 0); } } public boolean isOpen() { return mIsOpen; } @RequiresApi(api = Build.VERSION_CODES.KITKAT) private class OpenMorphTransition extends TransitionSet { OpenMorphTransition(ViewGroup viewGroup) { ChangeBounds changeBound = new ChangeBounds(); changeBound.excludeChildren(R.id.button_container, true); addTransition(changeBound); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { ChangeTransform changeTransform = new ChangeTransform(); for (int i = 0; i < viewGroup.getChildCount(); i++) { changeTransform.addTarget(viewGroup.getChildAt(i)); } addTransition(changeTransform); } setOrdering(TransitionSet.ORDERING_SEQUENTIAL); } } @RequiresApi(api = Build.VERSION_CODES.KITKAT) private class CloseMorphTransition extends TransitionSet { CloseMorphTransition(ViewGroup viewGroup) { ChangeBounds changeBound = new ChangeBounds(); changeBound.excludeChildren(R.id.button_container, true); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { ChangeTransform changeTransform = new ChangeTransform(); for (int i = 0; i < viewGroup.getChildCount(); i++) { changeTransform.addTarget(viewGroup.getChildAt(i)); } changeTransform.setDuration(CLOSE_MORPH_TRANSFORM_DURATION); addTransition(changeTransform); } addTransition(changeBound); setOrdering(TransitionSet.ORDERING_TOGETHER); } } }