package android.view.ext; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.animation.Animation; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; /** * Provides a "Path" like menu for android. ?? * * TODO: tell about usage * * @author Siyamed SINIR * */ public class SatelliteMenu extends FrameLayout { private static final int DEFAULT_SATELLITE_DISTANCE = 200; private static final float DEFAULT_TOTAL_SPACING_DEGREES = 90f; private static final boolean DEFAULT_CLOSE_ON_CLICK = true; private static final int DEFAULT_EXPAND_DURATION = 400; private Animation mainRotateRight; private Animation mainRotateLeft; private ImageView imgMain; private SateliteClickedListener itemClickedListener; private InternalSatelliteOnClickListener internalItemClickListener; private List<SatelliteMenuItem> menuItems = new ArrayList<SatelliteMenuItem>(); private Map<View, SatelliteMenuItem> viewToItemMap = new HashMap<View, SatelliteMenuItem>(); private AtomicBoolean plusAnimationActive = new AtomicBoolean(false); // ?? how to save/restore? private IDegreeProvider gapDegreesProvider = new DefaultDegreeProvider(); //States of these variables are saved private boolean rotated = false; private int measureDiff = 0; //States of these variables are saved - Also configured from XML private float totalSpacingDegree = DEFAULT_TOTAL_SPACING_DEGREES; private int satelliteDistance = DEFAULT_SATELLITE_DISTANCE; private int expandDuration = DEFAULT_EXPAND_DURATION; private boolean closeItemsOnClick = DEFAULT_CLOSE_ON_CLICK; public SatelliteMenu(Context context) { super(context); init(context, null, 0); } public SatelliteMenu(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0); } public SatelliteMenu(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs, defStyle); } private void init(Context context, AttributeSet attrs, int defStyle) { LayoutInflater.from(context).inflate(R.layout.sat_main, this, true); imgMain = (ImageView) findViewById(R.id.sat_main); if(attrs != null){ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SatelliteMenu, defStyle, 0); satelliteDistance = typedArray.getDimensionPixelSize(R.styleable.SatelliteMenu_satelliteDistance, DEFAULT_SATELLITE_DISTANCE); totalSpacingDegree = typedArray.getFloat(R.styleable.SatelliteMenu_totalSpacingDegree, DEFAULT_TOTAL_SPACING_DEGREES); closeItemsOnClick = typedArray.getBoolean(R.styleable.SatelliteMenu_closeOnClick, DEFAULT_CLOSE_ON_CLICK); expandDuration = typedArray.getInt(R.styleable.SatelliteMenu_expandDuration, DEFAULT_EXPAND_DURATION); //float satelliteDistance = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 170, getResources().getDisplayMetrics()); typedArray.recycle(); } mainRotateLeft = SatelliteAnimationCreator.createMainButtonAnimation(context); mainRotateRight = SatelliteAnimationCreator.createMainButtonInverseAnimation(context); Animation.AnimationListener plusAnimationListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { plusAnimationActive.set(false); } }; mainRotateLeft.setAnimationListener(plusAnimationListener); mainRotateRight.setAnimationListener(plusAnimationListener); imgMain.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { SatelliteMenu.this.onClick(); } }); internalItemClickListener = new InternalSatelliteOnClickListener(this); } private void onClick() { if (plusAnimationActive.compareAndSet(false, true)) { if (!rotated) { imgMain.startAnimation(mainRotateLeft); for (SatelliteMenuItem item : menuItems) { item.getView().startAnimation(item.getOutAnimation()); } } else { imgMain.startAnimation(mainRotateRight); for (SatelliteMenuItem item : menuItems) { item.getView().startAnimation(item.getInAnimation()); } } rotated = !rotated; } } private void openItems() { if (plusAnimationActive.compareAndSet(false, true)) { if (!rotated) { imgMain.startAnimation(mainRotateLeft); for (SatelliteMenuItem item : menuItems) { item.getView().startAnimation(item.getOutAnimation()); } } rotated = !rotated; } } private void closeItems() { if (plusAnimationActive.compareAndSet(false, true)) { if (rotated) { imgMain.startAnimation(mainRotateRight); for (SatelliteMenuItem item : menuItems) { item.getView().startAnimation(item.getInAnimation()); } } rotated = !rotated; } } public void addItems(List<SatelliteMenuItem> items) { menuItems.addAll(items); this.removeView(imgMain); TextView tmpView = new TextView(getContext()); tmpView.setLayoutParams(new FrameLayout.LayoutParams(0, 0)); float[] degrees = getDegrees(menuItems.size()); int index = 0; for (SatelliteMenuItem menuItem : menuItems) { int finalX = SatelliteAnimationCreator.getTranslateX( degrees[index], satelliteDistance); int finalY = SatelliteAnimationCreator.getTranslateY( degrees[index], satelliteDistance); ImageView itemView = (ImageView) LayoutInflater.from(getContext()) .inflate(R.layout.sat_item_cr, this, false); ImageView cloneView = (ImageView) LayoutInflater.from(getContext()) .inflate(R.layout.sat_item_cr, this, false); itemView.setTag(menuItem.getId()); cloneView.setVisibility(View.GONE); itemView.setVisibility(View.GONE); cloneView.setOnClickListener(internalItemClickListener); cloneView.setTag(Integer.valueOf(menuItem.getId())); FrameLayout.LayoutParams layoutParams = getLayoutParams(cloneView); layoutParams.bottomMargin = Math.abs(finalY); layoutParams.leftMargin = Math.abs(finalX); cloneView.setLayoutParams(layoutParams); if (menuItem.getImgResourceId() > 0) { itemView.setImageResource(menuItem.getImgResourceId()); cloneView.setImageResource(menuItem.getImgResourceId()); } else if (menuItem.getImgDrawable() != null) { itemView.setImageDrawable(menuItem.getImgDrawable()); cloneView.setImageDrawable(menuItem.getImgDrawable()); } Animation itemOut = SatelliteAnimationCreator.createItemOutAnimation(getContext(), index,expandDuration, finalX, finalY); Animation itemIn = SatelliteAnimationCreator.createItemInAnimation(getContext(), index, expandDuration, finalX, finalY); Animation itemClick = SatelliteAnimationCreator.createItemClickAnimation(getContext()); menuItem.setView(itemView); menuItem.setCloneView(cloneView); menuItem.setInAnimation(itemIn); menuItem.setOutAnimation(itemOut); menuItem.setClickAnimation(itemClick); menuItem.setFinalX(finalX); menuItem.setFinalY(finalY); itemIn.setAnimationListener(new SatelliteAnimationListener(itemView, true, viewToItemMap)); itemOut.setAnimationListener(new SatelliteAnimationListener(itemView, false, viewToItemMap)); itemClick.setAnimationListener(new SatelliteItemClickAnimationListener(this, menuItem.getId())); this.addView(itemView); this.addView(cloneView); viewToItemMap.put(itemView, menuItem); viewToItemMap.put(cloneView, menuItem); index++; } this.addView(imgMain); } private float[] getDegrees(int count) { return gapDegreesProvider.getDegrees(count, totalSpacingDegree); } private void recalculateMeasureDiff() { int itemWidth = 0; if (menuItems.size() > 0) { itemWidth = menuItems.get(0).getView().getWidth(); } measureDiff = Float.valueOf(satelliteDistance * 0.2f).intValue() + itemWidth; } private void resetItems() { if (menuItems.size() > 0) { List<SatelliteMenuItem> items = new ArrayList<SatelliteMenuItem>( menuItems); menuItems.clear(); this.removeAllViews(); addItems(items); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); recalculateMeasureDiff(); int totalHeight = imgMain.getHeight() + satelliteDistance + measureDiff; int totalWidth = imgMain.getWidth() + satelliteDistance + measureDiff; setMeasuredDimension(totalWidth, totalHeight); } private static class SatelliteItemClickAnimationListener implements Animation.AnimationListener { private WeakReference<SatelliteMenu> menuRef; private int tag; public SatelliteItemClickAnimationListener(SatelliteMenu menu, int tag) { this.menuRef = new WeakReference<SatelliteMenu>(menu); this.tag = tag; } @Override public void onAnimationEnd(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationStart(Animation animation) { SatelliteMenu menu = menuRef.get(); if(menu != null && menu.closeItemsOnClick){ menu.close(); if(menu.itemClickedListener != null){ menu.itemClickedListener.eventOccured(tag); } } } } private static class SatelliteAnimationListener implements Animation.AnimationListener { private WeakReference<View> viewRef; private boolean isInAnimation; private Map<View, SatelliteMenuItem> viewToItemMap; public SatelliteAnimationListener(View view, boolean isIn, Map<View, SatelliteMenuItem> viewToItemMap) { this.viewRef = new WeakReference<View>(view); this.isInAnimation = isIn; this.viewToItemMap = viewToItemMap; } @Override public void onAnimationStart(Animation animation) { if (viewRef != null) { View view = viewRef.get(); if (view != null) { SatelliteMenuItem menuItem = viewToItemMap.get(view); if (isInAnimation) { menuItem.getView().setVisibility(View.VISIBLE); menuItem.getCloneView().setVisibility(View.GONE); } else { menuItem.getCloneView().setVisibility(View.GONE); menuItem.getView().setVisibility(View.VISIBLE); } } } } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (viewRef != null) { View view = viewRef.get(); if (view != null) { SatelliteMenuItem menuItem = viewToItemMap.get(view); if (isInAnimation) { menuItem.getView().setVisibility(View.GONE); menuItem.getCloneView().setVisibility(View.GONE); } else { menuItem.getCloneView().setVisibility(View.VISIBLE); menuItem.getView().setVisibility(View.GONE); } } } } } public Map<View, SatelliteMenuItem> getViewToItemMap() { return viewToItemMap; } private static FrameLayout.LayoutParams getLayoutParams(View view) { return (FrameLayout.LayoutParams) view.getLayoutParams(); } private static class InternalSatelliteOnClickListener implements View.OnClickListener { private WeakReference<SatelliteMenu> menuRef; public InternalSatelliteOnClickListener(SatelliteMenu menu) { this.menuRef = new WeakReference<SatelliteMenu>(menu); } @Override public void onClick(View v) { SatelliteMenu menu = menuRef.get(); if(menu != null){ SatelliteMenuItem menuItem = menu.getViewToItemMap().get(v); v.startAnimation(menuItem.getClickAnimation()); } } } /** * Sets the click listener for satellite items. * * @param itemClickedListener */ public void setOnItemClickedListener(SateliteClickedListener itemClickedListener) { this.itemClickedListener = itemClickedListener; } /** * Defines the algorithm to define the gap between each item. * Note: Calling before adding items is strongly recommended. * * @param gapDegreeProvider */ public void setGapDegreeProvider(IDegreeProvider gapDegreeProvider) { this.gapDegreesProvider = gapDegreeProvider; resetItems(); } /** * Defines the total space between the initial and final item in degrees. * Note: Calling before adding items is strongly recommended. * * @param totalSpacingDegree The degree between initial and final items. */ public void setTotalSpacingDegree(float totalSpacingDegree) { this.totalSpacingDegree = totalSpacingDegree; resetItems(); } /** * Sets the distance of items from the center in pixels. * Note: Calling before adding items is strongly recommended. * * @param distance the distance of items to center in pixels. */ public void setSatelliteDistance(int distance) { this.satelliteDistance = distance; resetItems(); } /** * Sets the duration for expanding and collapsing the items in miliseconds. * Note: Calling before adding items is strongly recommended. * * @param expandDuration the duration for expanding and collapsing the items in miliseconds. */ public void setExpandDuration(int expandDuration) { this.expandDuration = expandDuration; resetItems(); } /** * Sets the image resource for the center button. * * @param resource The image resource. */ public void setMainImage(int resource) { this.imgMain.setImageResource(resource); } /** * Sets the image drawable for the center button. * * @param resource The image drawable. */ public void setMainImage(Drawable drawable) { this.imgMain.setImageDrawable(drawable); } /** * Defines if the menu shall collapse the items when an item is clicked. Default value is true. * * @param closeItemsOnClick */ public void setCloseItemsOnClick(boolean closeItemsOnClick) { this.closeItemsOnClick = closeItemsOnClick; } /** * The listener class for item click event. * @author Siyamed SINIR */ public interface SateliteClickedListener { /** * When an item is clicked, informs with the id of the item, which is given while adding the items. * * @param id The id of the item. */ public void eventOccured(int id); } /** * Expand the menu items. */ public void expand() { openItems(); } /** * Collapse the menu items */ public void close() { closeItems(); } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.rotated = rotated; ss.totalSpacingDegree = totalSpacingDegree; ss.satelliteDistance = satelliteDistance; ss.measureDiff = measureDiff; ss.expandDuration = expandDuration; ss.closeItemsOnClick = closeItemsOnClick; return ss; } @Override protected void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; rotated = ss.rotated; totalSpacingDegree = ss.totalSpacingDegree; satelliteDistance = ss.satelliteDistance; measureDiff = ss.measureDiff; expandDuration = ss.expandDuration; closeItemsOnClick = ss.closeItemsOnClick; super.onRestoreInstanceState(ss.getSuperState()); } static class SavedState extends BaseSavedState { boolean rotated; private float totalSpacingDegree; private int satelliteDistance; private int measureDiff; private int expandDuration; private boolean closeItemsOnClick; SavedState(Parcelable superState) { super(superState); } public SavedState(Parcel in) { super(in); rotated = Boolean.valueOf(in.readString()); totalSpacingDegree = in.readFloat(); satelliteDistance = in.readInt(); measureDiff = in.readInt(); expandDuration = in.readInt(); closeItemsOnClick = Boolean.valueOf(in.readString()); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel out, int flags) { out.writeString(Boolean.toString(rotated)); out.writeFloat(totalSpacingDegree); out.writeInt(satelliteDistance); out.writeInt(measureDiff); out.writeInt(expandDuration); out.writeString(Boolean.toString(closeItemsOnClick)); } public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }