diff --git a/DroidFish/res/drawable-hdpi/fab_bg_mini.png b/DroidFish/res/drawable-hdpi/fab_bg_mini.png
new file mode 100644
index 0000000..a5984b0
Binary files /dev/null and b/DroidFish/res/drawable-hdpi/fab_bg_mini.png differ
diff --git a/DroidFish/res/drawable-hdpi/fab_bg_normal.png b/DroidFish/res/drawable-hdpi/fab_bg_normal.png
new file mode 100644
index 0000000..fa69e8b
Binary files /dev/null and b/DroidFish/res/drawable-hdpi/fab_bg_normal.png differ
diff --git a/DroidFish/res/drawable-mdpi/fab_bg_mini.png b/DroidFish/res/drawable-mdpi/fab_bg_mini.png
new file mode 100644
index 0000000..c410597
Binary files /dev/null and b/DroidFish/res/drawable-mdpi/fab_bg_mini.png differ
diff --git a/DroidFish/res/drawable-mdpi/fab_bg_normal.png b/DroidFish/res/drawable-mdpi/fab_bg_normal.png
new file mode 100644
index 0000000..eafe4ba
Binary files /dev/null and b/DroidFish/res/drawable-mdpi/fab_bg_normal.png differ
diff --git a/DroidFish/res/drawable-xhdpi/drop_shadow.9.png b/DroidFish/res/drawable-xhdpi/drop_shadow.9.png
new file mode 100644
index 0000000..38081a1
Binary files /dev/null and b/DroidFish/res/drawable-xhdpi/drop_shadow.9.png differ
diff --git a/DroidFish/res/drawable-xhdpi/fab_bg_mini.png b/DroidFish/res/drawable-xhdpi/fab_bg_mini.png
new file mode 100644
index 0000000..350da4a
Binary files /dev/null and b/DroidFish/res/drawable-xhdpi/fab_bg_mini.png differ
diff --git a/DroidFish/res/drawable-xhdpi/fab_bg_normal.png b/DroidFish/res/drawable-xhdpi/fab_bg_normal.png
new file mode 100644
index 0000000..28125e1
Binary files /dev/null and b/DroidFish/res/drawable-xhdpi/fab_bg_normal.png differ
diff --git a/DroidFish/res/drawable-xxhdpi/fab_bg_mini.png b/DroidFish/res/drawable-xxhdpi/fab_bg_mini.png
new file mode 100644
index 0000000..4642841
Binary files /dev/null and b/DroidFish/res/drawable-xxhdpi/fab_bg_mini.png differ
diff --git a/DroidFish/res/drawable-xxhdpi/fab_bg_normal.png b/DroidFish/res/drawable-xxhdpi/fab_bg_normal.png
new file mode 100644
index 0000000..f1acbc0
Binary files /dev/null and b/DroidFish/res/drawable-xxhdpi/fab_bg_normal.png differ
diff --git a/DroidFish/res/drawable-xxxhdpi/fab_bg_mini.png b/DroidFish/res/drawable-xxxhdpi/fab_bg_mini.png
new file mode 100644
index 0000000..d4d7e2f
Binary files /dev/null and b/DroidFish/res/drawable-xxxhdpi/fab_bg_mini.png differ
diff --git a/DroidFish/res/drawable-xxxhdpi/fab_bg_normal.png b/DroidFish/res/drawable-xxxhdpi/fab_bg_normal.png
new file mode 100644
index 0000000..c488e55
Binary files /dev/null and b/DroidFish/res/drawable-xxxhdpi/fab_bg_normal.png differ
diff --git a/DroidFish/res/layout-land/main.xml b/DroidFish/res/layout-land/main.xml
index c1f108e..7d02e36 100644
--- a/DroidFish/res/layout-land/main.xml
+++ b/DroidFish/res/layout-land/main.xml
@@ -26,6 +26,7 @@
layout="@layout/title">
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DroidFish/res/raw/about.html b/DroidFish/res/raw/about.html
index c0331f9..34e9e04 100644
--- a/DroidFish/res/raw/about.html
+++ b/DroidFish/res/raw/about.html
@@ -240,6 +240,12 @@
Syzygy tablebases probing code, Copyright © 2011-2013 Ronald de Man.
+
+ TourGuide library, Copyright © 2015 Tan Jun Rong.
+
+
+ Floating action button library, Copyright © 2014 str4d and Jerzy Chalupski.
+
Translations
diff --git a/DroidFish/res/values-v14/colors.xml b/DroidFish/res/values-v14/colors.xml
new file mode 100644
index 0000000..78e2bd8
--- /dev/null
+++ b/DroidFish/res/values-v14/colors.xml
@@ -0,0 +1,7 @@
+
+
+ @android:color/holo_blue_dark
+ @android:color/holo_blue_light
+ @android:color/darker_gray
+ #FFFFFF
+
diff --git a/DroidFish/res/values/attrs.xml b/DroidFish/res/values/attrs.xml
new file mode 100644
index 0000000..b9eaaf5
--- /dev/null
+++ b/DroidFish/res/values/attrs.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DroidFish/res/values/colors.xml b/DroidFish/res/values/colors.xml
new file mode 100644
index 0000000..fdae922
--- /dev/null
+++ b/DroidFish/res/values/colors.xml
@@ -0,0 +1,7 @@
+
+
+ #ff0099cc
+ #ff33b5e5
+ #aaa
+ #FFFFFF
+
diff --git a/DroidFish/res/values/dimens.xml b/DroidFish/res/values/dimens.xml
new file mode 100644
index 0000000..a2b506e
--- /dev/null
+++ b/DroidFish/res/values/dimens.xml
@@ -0,0 +1,17 @@
+
+ 56dp
+ 40dp
+
+ 24dp
+
+ 14dp
+ 2dp
+
+ 3dp
+ 9dp
+
+ 1dp
+
+ 16dp
+ 8dp
+
diff --git a/DroidFish/res/values/ids.xml b/DroidFish/res/values/ids.xml
new file mode 100644
index 0000000..9b7c8b2
--- /dev/null
+++ b/DroidFish/res/values/ids.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/DroidFish/res/values/strings.xml b/DroidFish/res/values/strings.xml
index c67b913..4fb7ef9 100644
--- a/DroidFish/res/values/strings.xml
+++ b/DroidFish/res/values/strings.xml
@@ -413,6 +413,20 @@ you are not actively using the program.\
Directory where Syzygy tablebases are installed. Leave blank to use default directory
Syzygy Network Directory
Directory for network engines where Syzygy tablebases are installed.
+ Startup Guide
+ Show the startup guide the next time the program is started.
+ Left Menu
+ To open the left menu, tap on the left side of the title bar or swipe from the left side of the screen towards the right side.
+ Right Menu
+ To open the right menu, tap on the right side of the title bar or swipe from the right side of the screen towards the left side.
+ Chess Board
+ Touch and hold the chess board to open the tools menu.
+ Buttons
+ Tap a button to invoke its action. Touch and hold a button to open a menu containing secondary actions. To configure button actions go to Left Menu > Settings > Behavior > Configure Buttons.
+ Move List
+ Tap a move in the move list to set the chess board to the corresponding position. Touch and hold the move list to open the Edit Game menu.
+ Analysis information
+ When the engine is thinking, touch and hold the analysis information to open the Analysis menu.
@string/prefs_custom_button_1
@string/prefs_custom_button_2
@string/prefs_custom_button_3
diff --git a/DroidFish/res/xml/preferences.xml b/DroidFish/res/xml/preferences.xml
index 26931b9..0cdc938 100644
--- a/DroidFish/res/xml/preferences.xml
+++ b/DroidFish/res/xml/preferences.xml
@@ -518,6 +518,12 @@
+
+
= VERSION_CODES.JELLY_BEAN) {
+ setBackground(drawable);
+ } else {
+ setBackgroundDrawable(drawable);
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ TextView label = getLabelView();
+ if (label != null) {
+ label.setVisibility(visibility);
+ }
+
+ super.setVisibility(visibility);
+ }
+}
diff --git a/DroidFish/src/org/petero/droidfish/DroidFish.java b/DroidFish/src/org/petero/droidfish/DroidFish.java
index 40b0905..5973f3e 100644
--- a/DroidFish/src/org/petero/droidfish/DroidFish.java
+++ b/DroidFish/src/org/petero/droidfish/DroidFish.java
@@ -60,6 +60,12 @@ import org.petero.droidfish.gamelogic.TimeControlData;
import org.petero.droidfish.tb.Probe;
import org.petero.droidfish.tb.ProbeResult;
+import tourguide.tourguide.Overlay;
+import tourguide.tourguide.Pointer;
+import tourguide.tourguide.Sequence;
+import tourguide.tourguide.ToolTip;
+import tourguide.tourguide.TourGuide;
+
import com.kalab.chess.enginesupport.ChessEngine;
import com.kalab.chess.enginesupport.ChessEngineResolver;
import com.larvalabs.svgandroid.SVG;
@@ -191,7 +197,9 @@ public class DroidFish extends Activity
private TextView status;
private ScrollView moveListScroll;
private MoveListView moveList;
+ private View thinkingScroll;
private TextView thinking;
+ private View buttons;
private ImageButton custom1Button, custom2Button, custom3Button;
private ImageButton modeButton, undoButton, redoButton;
private ButtonActions custom1ButtonActions, custom2ButtonActions, custom3ButtonActions;
@@ -254,6 +262,9 @@ public class DroidFish extends Activity
private Typeface figNotation;
private Typeface defaultThinkingListTypeFace;
+ private boolean guideShowOnStart;
+ private TourGuide tourGuide;
+
/** Defines all configurable button actions. */
private ActionFactory actionFactory = new ActionFactory() {
@@ -486,6 +497,94 @@ public class DroidFish extends Activity
else
loadPGNFromFile(intentFilename);
}
+
+ startTourGuide();
+ }
+
+ private void startTourGuide(){
+ if (!guideShowOnStart)
+ return;
+
+ tourGuide = TourGuide.init(this);
+ ArrayList guides = new ArrayList();
+
+ TourGuide tg = TourGuide.init(this);
+ tg.setToolTip(new ToolTip()
+ .setTitle(getString(R.string.tour_leftMenu_title))
+ .setDescription(getString(R.string.tour_leftMenu_desc))
+ .setGravity(Gravity.BOTTOM | Gravity.RIGHT));
+ tg.playLater(whiteTitleText);
+ guides.add(tg);
+
+ tg = TourGuide.init(this);
+ tg.setToolTip(new ToolTip()
+ .setTitle(getString(R.string.tour_rightMenu_title))
+ .setDescription(getString(R.string.tour_rightMenu_desc))
+ .setGravity(Gravity.BOTTOM | Gravity.LEFT));
+ tg.playLater(blackTitleText);
+ guides.add(tg);
+
+ tg = TourGuide.init(this);
+ int gravity = !landScapeView() ? Gravity.BOTTOM : leftHandedView() ? Gravity.LEFT : Gravity.RIGHT;
+ tg.setToolTip(new ToolTip()
+ .setTitle(getString(R.string.tour_chessBoard_title))
+ .setDescription(getString(R.string.tour_chessBoard_desc))
+ .setGravity(gravity));
+ tg.playLater(cb);
+ guides.add(tg);
+
+ tg = TourGuide.init(this);
+ gravity = !landScapeView() ? Gravity.TOP : Gravity.BOTTOM;
+ tg.setToolTip(new ToolTip()
+ .setTitle(getString(R.string.tour_buttons_title))
+ .setDescription(getString(R.string.tour_buttons_desc))
+ .setGravity(gravity));
+ tg.playLater(buttons);
+ guides.add(tg);
+
+ tg = TourGuide.init(this);
+ gravity = !landScapeView() ? Gravity.TOP : leftHandedView() ? Gravity.RIGHT : Gravity.LEFT;
+ tg.setToolTip(new ToolTip()
+ .setTitle(getString(R.string.tour_moveList_title))
+ .setDescription(getString(R.string.tour_moveList_desc))
+ .setGravity(gravity));
+ tg.playLater(moveListScroll);
+ guides.add(tg);
+
+ tg = TourGuide.init(this);
+ tg.setToolTip(new ToolTip()
+ .setTitle(getString(R.string.tour_analysis_title))
+ .setDescription(getString(R.string.tour_analysis_desc))
+ .setGravity(Gravity.TOP));
+ tg.playLater(thinkingScroll);
+ guides.add(tg);
+
+ tg.setOverlay(new Overlay()
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ guideShowOnStart = false;
+ Editor editor = settings.edit();
+ editor.putBoolean("guideShowOnStart", false);
+ editor.commit();
+ tourGuide.next();
+ tourGuide = null;
+ }
+ }));
+
+ Sequence sequence = new Sequence.SequenceBuilder()
+ .add(guides.toArray(new TourGuide[guides.size()]))
+ .setDefaultOverlay(new Overlay()
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ tourGuide.next();
+ }
+ }))
+ .setDefaultPointer(new Pointer())
+ .setContinueMethod(Sequence.ContinueMethod.OverlayListener)
+ .build();
+ tourGuide.playInSequence(sequence);
}
// Unicode code points for chess pieces
@@ -664,12 +763,20 @@ public class DroidFish extends Activity
updateThinkingInfo();
ctrl.updateRemainingTime();
ctrl.updateMaterialDiffList();
+ if (tourGuide != null) {
+ tourGuide.cleanUp();
+ tourGuide = null;
+ }
}
+ /** Return true if the current orientation is landscape. */
+ private final boolean landScapeView() {
+ return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+ }
+
/** Return true if left-handed layout should be used. */
private final boolean leftHandedView() {
- return settings.getBoolean("leftHanded", false) &&
- (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
+ return settings.getBoolean("leftHanded", false) && landScapeView();
}
/** Re-read preferences settings. */
@@ -708,6 +815,7 @@ public class DroidFish extends Activity
status = (TextView)findViewById(R.id.status);
moveListScroll = (ScrollView)findViewById(R.id.scrollView);
moveList = (MoveListView)findViewById(R.id.moveList);
+ thinkingScroll = (View)findViewById(R.id.scrollViewBot);
thinking = (TextView)findViewById(R.id.thinking);
defaultThinkingListTypeFace = thinking.getTypeface();
status.setFocusable(false);
@@ -909,6 +1017,7 @@ public class DroidFish extends Activity
}
});
+ buttons = (View)findViewById(R.id.buttons);
custom1Button = (ImageButton)findViewById(R.id.custom1Button);
custom1ButtonActions.setImageButton(custom1Button, this);
custom2Button = (ImageButton)findViewById(R.id.custom2Button);
@@ -1078,6 +1187,8 @@ public class DroidFish extends Activity
custom3ButtonActions.readPrefs(settings, actionFactory);
updateButtons();
+ guideShowOnStart = settings.getBoolean("guideShowOnStart", true);
+
bookOptions.filename = settings.getString("bookFile", "");
bookOptions.maxLength = getIntSetting("bookMaxLength", 1000000);
bookOptions.preferMainLines = settings.getBoolean("bookPreferMainLines", false);
diff --git a/DroidFish/src/tourguide/tourguide/FrameLayoutWithHole.java b/DroidFish/src/tourguide/tourguide/FrameLayoutWithHole.java
new file mode 100644
index 0000000..69eaf48
--- /dev/null
+++ b/DroidFish/src/tourguide/tourguide/FrameLayoutWithHole.java
@@ -0,0 +1,311 @@
+package tourguide.tourguide;
+
+import android.animation.AnimatorSet;
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.support.v4.view.MotionEventCompat;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.widget.FrameLayout;
+import java.util.ArrayList;
+
+/**
+ * TODO: document your custom view class.
+ */
+public class FrameLayoutWithHole extends FrameLayout {
+ private TextPaint mTextPaint;
+ private Activity mActivity;
+ private TourGuide.MotionType mMotionType;
+ private Paint mEraser;
+
+ Bitmap mEraserBitmap;
+ private Canvas mEraserCanvas;
+ private Paint mPaint;
+ private Paint transparentPaint;
+ private View mViewHole; // This is the targeted view to be highlighted, where the hole should be placed
+ private int mRadius;
+ private int [] mPos;
+ private float mDensity;
+ private Overlay mOverlay;
+
+ private ArrayList mAnimatorSetArrayList;
+
+ public void setViewHole(View viewHole) {
+ this.mViewHole = viewHole;
+ enforceMotionType();
+ }
+ public void addAnimatorSet(AnimatorSet animatorSet){
+ if (mAnimatorSetArrayList==null){
+ mAnimatorSetArrayList = new ArrayList();
+ }
+ mAnimatorSetArrayList.add(animatorSet);
+ }
+ private void enforceMotionType(){
+ Log.d("tourguide", "enforceMotionType 1");
+ if (mViewHole!=null) {Log.d("tourguide","enforceMotionType 2");
+ if (mMotionType!=null && mMotionType == TourGuide.MotionType.ClickOnly) {
+ Log.d("tourguide","enforceMotionType 3");
+ Log.d("tourguide","only Clicking");
+ mViewHole.setOnTouchListener(new OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ mViewHole.getParent().requestDisallowInterceptTouchEvent(true);
+ return false;
+ }
+ });
+ } else if (mMotionType!=null && mMotionType == TourGuide.MotionType.SwipeOnly) {
+ Log.d("tourguide","enforceMotionType 4");
+ Log.d("tourguide","only Swiping");
+ mViewHole.setClickable(false);
+ }
+ }
+ }
+
+ public FrameLayoutWithHole(Activity context, View view) {
+ this(context, view, TourGuide.MotionType.AllowAll);
+ }
+ public FrameLayoutWithHole(Activity context, View view, TourGuide.MotionType motionType) {
+ this(context, view, motionType, new Overlay());
+ }
+
+ public FrameLayoutWithHole(Activity context, View view, TourGuide.MotionType motionType, Overlay overlay) {
+ super(context);
+ mActivity = context;
+ mViewHole = view;
+ init(null, 0);
+ enforceMotionType();
+ mOverlay = overlay;
+
+ int [] pos = new int[2];
+ mViewHole.getLocationOnScreen(pos);
+ mPos = pos;
+
+ mDensity = context.getResources().getDisplayMetrics().density;
+ int padding = (int)(20 * mDensity);
+
+ if (mViewHole.getHeight() > mViewHole.getWidth()) {
+ mRadius = mViewHole.getHeight()/2 + padding;
+ } else {
+ mRadius = mViewHole.getWidth()/2 + padding;
+ }
+ mMotionType = motionType;
+ }
+ private void init(AttributeSet attrs, int defStyle) {
+ // Load attributes
+// final TypedArray a = getContext().obtainStyledAttributes(
+// attrs, FrameLayoutWithHole, defStyle, 0);
+//
+//
+// a.recycle();
+ setWillNotDraw(false);
+ // Set up a default TextPaint object
+ mTextPaint = new TextPaint();
+ mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
+ mTextPaint.setTextAlign(Paint.Align.LEFT);
+
+ Point size = new Point();
+ size.x = mActivity.getResources().getDisplayMetrics().widthPixels;
+ size.y = mActivity.getResources().getDisplayMetrics().heightPixels;
+
+ mEraserBitmap = Bitmap.createBitmap(size.x, size.y, Bitmap.Config.ARGB_8888);
+ mEraserCanvas = new Canvas(mEraserBitmap);
+
+ mPaint = new Paint();
+ mPaint.setColor(0xcc000000);
+ transparentPaint = new Paint();
+ transparentPaint.setColor(getResources().getColor(android.R.color.transparent));
+ transparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+
+ mEraser = new Paint();
+ mEraser.setColor(0xFFFFFFFF);
+ mEraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+ mEraser.setFlags(Paint.ANTI_ALIAS_FLAG);
+
+ Log.d("tourguide","getHeight: "+ size.y);
+ Log.d("tourguide","getWidth: " + size.x);
+
+ }
+
+ private boolean mCleanUpLock = false;
+ protected void cleanUp(){
+ if (getParent() != null) {
+ if (mOverlay!=null && mOverlay.mExitAnimation!=null) {
+ performOverlayExitAnimation();
+ } else {
+ ((ViewGroup) this.getParent()).removeView(this);
+ }
+ }
+ }
+ private void performOverlayExitAnimation(){
+ if (!mCleanUpLock) {
+ final FrameLayout _pointerToFrameLayout = this;
+ mCleanUpLock = true;
+ Log.d("tourguide","Overlay exit animation listener is overwritten...");
+ mOverlay.mExitAnimation.setAnimationListener(new Animation.AnimationListener() {
+ @Override public void onAnimationStart(Animation animation) {}
+ @Override public void onAnimationRepeat(Animation animation) {}
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ ((ViewGroup) _pointerToFrameLayout.getParent()).removeView(_pointerToFrameLayout);
+ }
+ });
+ this.startAnimation(mOverlay.mExitAnimation);
+ }
+ }
+ /* comment this whole method to cause a memory leak */
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ /* cleanup reference to prevent memory leak */
+ mEraserCanvas.setBitmap(null);
+ mEraserBitmap = null;
+
+ if (mAnimatorSetArrayList != null && mAnimatorSetArrayList.size() > 0){
+ for(int i=0;i> MotionEvent.ACTION_POINTER_ID_SHIFT);
+ sb.append(")" );
+ }
+ sb.append("[" );
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ sb.append("#" ).append(i);
+ sb.append("(pid " ).append(event.getPointerId(i));
+ sb.append(")=" ).append((int) event.getX(i));
+ sb.append("," ).append((int) event.getY(i));
+ if (i + 1 < event.getPointerCount())
+ sb.append(";" );
+ }
+ sb.append("]" );
+ Log.d("tourguide", sb.toString());
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ //first check if the location button should handle the touch event
+ dumpEvent(ev);
+ int action = MotionEventCompat.getActionMasked(ev);
+ if(mViewHole != null) {
+ int[] pos = new int[2];
+ mViewHole.getLocationOnScreen(pos);
+ Log.d("tourguide", "[dispatchTouchEvent] mViewHole.getHeight(): "+mViewHole.getHeight());
+ Log.d("tourguide", "[dispatchTouchEvent] mViewHole.getWidth(): "+mViewHole.getWidth());
+
+ Log.d("tourguide", "[dispatchTouchEvent] Touch X(): "+ev.getRawX());
+ Log.d("tourguide", "[dispatchTouchEvent] Touch Y(): "+ev.getRawY());
+
+// Log.d("tourguide", "[dispatchTouchEvent] X of image: "+pos[0]);
+// Log.d("tourguide", "[dispatchTouchEvent] Y of image: "+pos[1]);
+
+ Log.d("tourguide", "[dispatchTouchEvent] X lower bound: "+ pos[0]);
+ Log.d("tourguide", "[dispatchTouchEvent] X higher bound: "+(pos[0] +mViewHole.getWidth()));
+
+ Log.d("tourguide", "[dispatchTouchEvent] Y lower bound: "+ pos[1]);
+ Log.d("tourguide", "[dispatchTouchEvent] Y higher bound: "+(pos[1] +mViewHole.getHeight()));
+
+ if(ev.getRawY() >= pos[1] && ev.getRawY() <= (pos[1] + mViewHole.getHeight()) && ev.getRawX() >= pos[0] && ev.getRawX() <= (pos[0] + mViewHole.getWidth())) { //location button event
+ Log.d("tourguide","to the BOTTOM!");
+ Log.d("tourguide",""+ev.getAction());
+
+// switch(action) {
+// case (MotionEvent.ACTION_DOWN) :
+// Log.d("tourguide","Action was DOWN");
+// return false;
+// case (MotionEvent.ACTION_MOVE) :
+// Log.d("tourguide","Action was MOVE");
+// return true;
+// case (MotionEvent.ACTION_UP) :
+// Log.d("tourguide","Action was UP");
+//// ev.setAction(MotionEvent.ACTION_DOWN|MotionEvent.ACTION_UP);
+//// return super.dispatchTouchEvent(ev);
+// return false;
+// case (MotionEvent.ACTION_CANCEL) :
+// Log.d("tourguide","Action was CANCEL");
+// return true;
+// case (MotionEvent.ACTION_OUTSIDE) :
+// Log.d("tourguide","Movement occurred outside bounds " +
+// "of current screen element");
+// return true;
+// default :
+// return super.dispatchTouchEvent(ev);
+// }
+// return mViewHole.onTouchEvent(ev);
+
+ return false;
+ }
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ mEraserBitmap.eraseColor(Color.TRANSPARENT);
+
+ if (mOverlay!=null) {
+ mEraserCanvas.drawColor(mOverlay.mBackgroundColor);
+ int padding = (int) (10 * mDensity);
+ if (mOverlay.mStyle == Overlay.Style.Rectangle) {
+ mEraserCanvas.drawRect(mPos[0] - padding, mPos[1] - padding, mPos[0] + mViewHole.getWidth() + padding, mPos[1] + mViewHole.getHeight() + padding, mEraser);
+ } else {
+ mEraserCanvas.drawCircle(mPos[0] + mViewHole.getWidth() / 2, mPos[1] + mViewHole.getHeight() / 2, mRadius, mEraser);
+ }
+ }
+ canvas.drawBitmap(mEraserBitmap, 0, 0, null);
+
+ }
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (mOverlay!=null && mOverlay.mEnterAnimation!=null) {
+ this.startAnimation(mOverlay.mEnterAnimation);
+ }
+ }
+ /**
+ *
+ * Convenient method to obtain screen width in pixel
+ *
+ * @param activity
+ * @return screen width in pixel
+ */
+ public int getScreenWidth(Activity activity){
+ return activity.getResources().getDisplayMetrics().widthPixels;
+ }
+
+ /**
+ *
+ * Convenient method to obtain screen height in pixel
+ *
+ * @param activity
+ * @return screen width in pixel
+ */
+ public int getScreenHeight(Activity activity){
+ return activity.getResources().getDisplayMetrics().heightPixels;
+ }
+}
diff --git a/DroidFish/src/tourguide/tourguide/Overlay.java b/DroidFish/src/tourguide/tourguide/Overlay.java
new file mode 100644
index 0000000..e53ebde
--- /dev/null
+++ b/DroidFish/src/tourguide/tourguide/Overlay.java
@@ -0,0 +1,83 @@
+package tourguide.tourguide;
+
+import android.graphics.Color;
+import android.view.View;
+import android.view.animation.Animation;
+
+/**
+ * Created by tanjunrong on 6/20/15.
+ */
+public class Overlay {
+ public int mBackgroundColor;
+ public boolean mDisableClick;
+ public Style mStyle;
+ public Animation mEnterAnimation, mExitAnimation;
+ public View.OnClickListener mOnClickListener;
+
+ public enum Style {
+ Circle, Rectangle
+ }
+ public Overlay() {
+ this(true, Color.parseColor("#55000000"), Style.Circle);
+ }
+
+ public Overlay(boolean disableClick, int backgroundColor, Style style) {
+ mDisableClick = disableClick;
+ mBackgroundColor = backgroundColor;
+ mStyle = style;
+ }
+
+ /**
+ * Set background color
+ * @param backgroundColor
+ * @return return ToolTip instance for chaining purpose
+ */
+ public Overlay setBackgroundColor(int backgroundColor){
+ mBackgroundColor = backgroundColor;
+ return this;
+ }
+
+ /**
+ * Set to true if you want to block all user input to pass through this overlay, set to false if you want to allow user input under the overlay
+ * @param yes_no
+ * @return return Overlay instance for chaining purpose
+ */
+ public Overlay disableClick(boolean yes_no){
+ mDisableClick = yes_no;
+ return this;
+ }
+
+ public Overlay setStyle(Style style){
+ mStyle = style;
+ return this;
+ }
+
+ /**
+ * Set enter animation
+ * @param enterAnimation
+ * @return return Overlay instance for chaining purpose
+ */
+ public Overlay setEnterAnimation(Animation enterAnimation){
+ mEnterAnimation = enterAnimation;
+ return this;
+ }
+ /**
+ * Set exit animation
+ * @param exitAnimation
+ * @return return Overlay instance for chaining purpose
+ */
+ public Overlay setExitAnimation(Animation exitAnimation){
+ mExitAnimation = exitAnimation;
+ return this;
+ }
+
+ /**
+ * Set onClickListener for the Overlay
+ * @param onClickListener
+ * @return return Overlay instance for chaining purpose
+ */
+ public Overlay setOnClickListener(View.OnClickListener onClickListener){
+ mOnClickListener=onClickListener;
+ return this;
+ }
+}
diff --git a/DroidFish/src/tourguide/tourguide/Pointer.java b/DroidFish/src/tourguide/tourguide/Pointer.java
new file mode 100644
index 0000000..c820277
--- /dev/null
+++ b/DroidFish/src/tourguide/tourguide/Pointer.java
@@ -0,0 +1,41 @@
+package tourguide.tourguide;
+
+import android.graphics.Color;
+import android.view.Gravity;
+
+/**
+ * Created by tanjunrong on 6/20/15.
+ */
+public class Pointer {
+ public int mGravity = Gravity.CENTER;
+ public int mColor = Color.WHITE;
+
+ public Pointer() {
+ this(Gravity.CENTER, Color.parseColor("#FFFFFF"));
+ }
+
+ public Pointer(int gravity, int color) {
+ this.mGravity = gravity;
+ this.mColor = color;
+ }
+
+ /**
+ * Set color
+ * @param color
+ * @return return Pointer instance for chaining purpose
+ */
+ public Pointer setColor(int color){
+ mColor = color;
+ return this;
+ }
+
+ /**
+ * Set gravity
+ * @param gravity
+ * @return return Pointer instance for chaining purpose
+ */
+ public Pointer setGravity(int gravity){
+ mGravity = gravity;
+ return this;
+ }
+}
diff --git a/DroidFish/src/tourguide/tourguide/Sequence.java b/DroidFish/src/tourguide/tourguide/Sequence.java
new file mode 100644
index 0000000..fc64f38
--- /dev/null
+++ b/DroidFish/src/tourguide/tourguide/Sequence.java
@@ -0,0 +1,219 @@
+package tourguide.tourguide;
+
+import android.view.View;
+
+/**
+ * Created by aaronliew on 8/7/15.
+ */
+public class Sequence {
+ TourGuide [] mTourGuideArray;
+ Overlay mDefaultOverlay;
+ ToolTip mDefaultToolTip;
+ Pointer mDefaultPointer;
+
+ ContinueMethod mContinueMethod;
+ boolean mDisableTargetButton;
+ public int mCurrentSequence;
+ TourGuide mParentTourGuide;
+ public enum ContinueMethod {
+ Overlay, OverlayListener
+ }
+ private Sequence(SequenceBuilder builder){
+ this.mTourGuideArray = builder.mTourGuideArray;
+ this.mDefaultOverlay = builder.mDefaultOverlay;
+ this.mDefaultToolTip = builder.mDefaultToolTip;
+ this.mDefaultPointer = builder.mDefaultPointer;
+ this.mContinueMethod = builder.mContinueMethod;
+ this.mCurrentSequence = builder.mCurrentSequence;
+
+ // TODO: to be implemented
+ this.mDisableTargetButton = builder.mDisableTargetButton;
+ }
+
+ /**
+ * sets the parent TourGuide that will run this Sequence
+ */
+ protected void setParentTourGuide(TourGuide parentTourGuide){
+ mParentTourGuide = parentTourGuide;
+
+ if(mContinueMethod == ContinueMethod.Overlay) {
+ for (final TourGuide tourGuide : mTourGuideArray) {
+ tourGuide.mOverlay.mOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mParentTourGuide.next();
+ }
+ };
+ }
+ }
+ }
+
+ public TourGuide getNextTourGuide() {
+ return mTourGuideArray[mCurrentSequence];
+ }
+
+ public ContinueMethod getContinueMethod() {
+ return mContinueMethod;
+ }
+
+ public TourGuide[] getTourGuideArray() {
+ return mTourGuideArray;
+ }
+
+ public Overlay getDefaultOverlay() {
+ return mDefaultOverlay;
+ }
+
+ public ToolTip getDefaultToolTip() {
+ return mDefaultToolTip;
+ }
+
+ public ToolTip getToolTip() {
+ // individual tour guide has higher priority
+ if (mTourGuideArray[mCurrentSequence].mToolTip != null){
+ return mTourGuideArray[mCurrentSequence].mToolTip;
+ } else {
+ return mDefaultToolTip;
+ }
+ }
+
+ public Overlay getOverlay() {
+ // Overlay is used as a method to proceed to next TourGuide, so the default overlay is already assigned appropriately if needed
+ return mTourGuideArray[mCurrentSequence].mOverlay;
+ }
+
+ public Pointer getPointer() {
+ // individual tour guide has higher priority
+ if (mTourGuideArray[mCurrentSequence].mPointer != null){
+ return mTourGuideArray[mCurrentSequence].mPointer;
+ } else {
+ return mDefaultPointer;
+ }
+ }
+
+ public static class SequenceBuilder {
+ TourGuide [] mTourGuideArray;
+ Overlay mDefaultOverlay;
+ ToolTip mDefaultToolTip;
+ Pointer mDefaultPointer;
+ ContinueMethod mContinueMethod;
+ int mCurrentSequence;
+ boolean mDisableTargetButton;
+
+ public SequenceBuilder add(TourGuide... tourGuideArray){
+ mTourGuideArray = tourGuideArray;
+ return this;
+ }
+
+ public SequenceBuilder setDefaultOverlay(Overlay defaultOverlay){
+ mDefaultOverlay = defaultOverlay;
+ return this;
+ }
+
+ // This might not be useful, but who knows.. maybe someone needs it
+ public SequenceBuilder setDefaultToolTip(ToolTip defaultToolTip){
+ mDefaultToolTip = defaultToolTip;
+ return this;
+ }
+
+ public SequenceBuilder setDefaultPointer(Pointer defaultPointer){
+ mDefaultPointer = defaultPointer;
+ return this;
+ }
+
+ // TODO: this is an uncompleted feature, make it private first
+ // This is intended to be used to disable the button, so people cannot click on in during a Tour, instead, people can only click on Next button or Overlay to proceed
+ private SequenceBuilder setDisableButton(boolean disableTargetButton){
+ mDisableTargetButton = disableTargetButton;
+ return this;
+ }
+
+ /**
+ * @param continueMethod ContinueMethod.Overlay or ContinueMethod.OverlayListener
+ * ContnueMethod.Overlay - clicking on Overlay will make TourGuide proceed to the next one.
+ * ContinueMethod.OverlayListener - you need to provide OverlayListeners, and call tourGuideHandler.next() in the listener to proceed to the next one.
+ */
+ public SequenceBuilder setContinueMethod(ContinueMethod continueMethod){
+ mContinueMethod = continueMethod;
+ return this;
+ }
+
+ public Sequence build(){
+ mCurrentSequence = 0;
+ checkIfContinueMethodNull();
+ checkAtLeastTwoTourGuideSupplied();
+ checkOverlayListener(mContinueMethod);
+
+ return new Sequence(this);
+ }
+ private void checkIfContinueMethodNull(){
+ if (mContinueMethod == null){
+ throw new IllegalArgumentException("Continue Method is not set. Please provide ContinueMethod in setContinueMethod");
+ }
+ }
+ private void checkAtLeastTwoTourGuideSupplied() {
+ if (mTourGuideArray == null || mTourGuideArray.length <= 1){
+ throw new IllegalArgumentException("In order to run a sequence, you must at least supply 2 TourGuide into Sequence using add()");
+ }
+ }
+ private void checkOverlayListener(ContinueMethod continueMethod) {
+ if(continueMethod == ContinueMethod.OverlayListener){
+ boolean pass = true;
+ if (mDefaultOverlay != null && mDefaultOverlay.mOnClickListener != null) {
+ pass = true;
+ // when default listener is available, we loop through individual tour guide, and
+ // assign default listener to individual tour guide
+ for (TourGuide tourGuide : mTourGuideArray) {
+ if (tourGuide.mOverlay == null) {
+ tourGuide.mOverlay = mDefaultOverlay;
+ }
+ if (tourGuide.mOverlay != null && tourGuide.mOverlay.mOnClickListener == null) {
+ tourGuide.mOverlay.mOnClickListener = mDefaultOverlay.mOnClickListener;
+ }
+ }
+ } else { // case where: default listener is not available
+
+ for (TourGuide tourGuide : mTourGuideArray) {
+ //Both of the overlay and default listener is not null, throw the error
+ if (tourGuide.mOverlay != null && tourGuide.mOverlay.mOnClickListener == null) {
+ pass = false;
+ break;
+ } else if (tourGuide.mOverlay == null){
+ pass = false;
+ break;
+ }
+ }
+
+ }
+
+ if (!pass){
+ throw new IllegalArgumentException("ContinueMethod.OverlayListener is chosen as the ContinueMethod, but no Default Overlay Listener is set, or not all Overlay.mListener is set for all the TourGuide passed in.");
+ }
+ } else if(continueMethod == ContinueMethod.Overlay){
+ // when Overlay ContinueMethod is used, listener must not be supplied (to avoid unexpected result)
+ boolean pass = true;
+ if (mDefaultOverlay != null && mDefaultOverlay.mOnClickListener != null) {
+ pass = false;
+ } else {
+ for (TourGuide tourGuide : mTourGuideArray) {
+ if (tourGuide.mOverlay != null && tourGuide.mOverlay.mOnClickListener != null ) {
+ pass = false;
+ break;
+ }
+ }
+ }
+ if (mDefaultOverlay != null) {
+ for (TourGuide tourGuide : mTourGuideArray) {
+ if (tourGuide.mOverlay == null) {
+ tourGuide.mOverlay = mDefaultOverlay;
+ }
+ }
+ }
+
+ if (!pass) {
+ throw new IllegalArgumentException("ContinueMethod.Overlay is chosen as the ContinueMethod, but either default overlay listener is still set OR individual overlay listener is still set, make sure to clear all Overlay listener");
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/DroidFish/src/tourguide/tourguide/ToolTip.java b/DroidFish/src/tourguide/tourguide/ToolTip.java
new file mode 100644
index 0000000..85ec0ef
--- /dev/null
+++ b/DroidFish/src/tourguide/tourguide/ToolTip.java
@@ -0,0 +1,119 @@
+package tourguide.tourguide;
+
+import android.graphics.Color;
+import android.view.Gravity;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.BounceInterpolator;
+
+/**
+ * Created by tanjunrong on 6/17/15.
+ */
+public class ToolTip {
+ public String mTitle, mDescription;
+ public int mBackgroundColor, mTextColor;
+ public Animation mEnterAnimation, mExitAnimation;
+ public boolean mShadow;
+ public int mGravity;
+ public View.OnClickListener mOnClickListener;
+
+ public ToolTip(){
+ /* default values */
+ mTitle = "";
+ mDescription = "";
+ mBackgroundColor = Color.parseColor("#3498db");
+ mTextColor = Color.parseColor("#FFFFFF");
+
+ mEnterAnimation = new AlphaAnimation(0f, 1f);
+ mEnterAnimation.setDuration(1000);
+ mEnterAnimation.setFillAfter(true);
+ mEnterAnimation.setInterpolator(new BounceInterpolator());
+ mShadow = true;
+
+ // TODO: exit animation
+ mGravity = Gravity.CENTER;
+ }
+ /**
+ * Set title text
+ * @param title
+ * @return return ToolTip instance for chaining purpose
+ */
+ public ToolTip setTitle(String title){
+ mTitle = title;
+ return this;
+ }
+
+ /**
+ * Set description text
+ * @param description
+ * @return return ToolTip instance for chaining purpose
+ */
+ public ToolTip setDescription(String description){
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * Set background color
+ * @param backgroundColor
+ * @return return ToolTip instance for chaining purpose
+ */
+ public ToolTip setBackgroundColor(int backgroundColor){
+ mBackgroundColor = backgroundColor;
+ return this;
+ }
+
+ /**
+ * Set text color
+ * @param textColor
+ * @return return ToolTip instance for chaining purpose
+ */
+ public ToolTip setTextColor(int textColor){
+ mTextColor = textColor;
+ return this;
+ }
+
+ /**
+ * Set enter animation
+ * @param enterAnimation
+ * @return return ToolTip instance for chaining purpose
+ */
+ public ToolTip setEnterAnimation(Animation enterAnimation){
+ mEnterAnimation = enterAnimation;
+ return this;
+ }
+ /**
+ * Set exit animation
+ * @param exitAnimation
+ * @return return ToolTip instance for chaining purpose
+ */
+// TODO:
+// public ToolTip setExitAnimation(Animation exitAnimation){
+// mExitAnimation = exitAnimation;
+// return this;
+// }
+ /**
+ * Set the gravity, the setGravity is centered relative to the targeted button
+ * @param gravity Gravity.CENTER, Gravity.TOP, Gravity.BOTTOM, etc
+ * @return return ToolTip instance for chaining purpose
+ */
+ public ToolTip setGravity(int gravity){
+ mGravity = gravity;
+ return this;
+ }
+ /**
+ * Set if you want to have setShadow
+ * @param shadow
+ * @return return ToolTip instance for chaining purpose
+ */
+ public ToolTip setShadow(boolean shadow){
+ mShadow = shadow;
+ return this;
+ }
+
+ public ToolTip setOnClickListener(View.OnClickListener onClickListener){
+ mOnClickListener = onClickListener;
+ return this;
+ }
+}
diff --git a/DroidFish/src/tourguide/tourguide/TourGuide.java b/DroidFish/src/tourguide/tourguide/TourGuide.java
new file mode 100644
index 0000000..c54beed
--- /dev/null
+++ b/DroidFish/src/tourguide/tourguide/TourGuide.java
@@ -0,0 +1,633 @@
+package tourguide.tourguide;
+
+import org.petero.droidfish.R;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.util.Log;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import net.i2p.android.ext.floatingactionbutton.FloatingActionButton;
+
+/**
+ * Created by tanjunrong on 2/10/15.
+ */
+public class TourGuide {
+ /**
+ * This describes the animation techniques
+ * */
+ public enum Technique {
+ Click, HorizontalLeft, HorizontalRight, VerticalUpward, VerticalDownward
+ }
+
+ /**
+ * This describes the allowable motion, for example if you want the users to learn about clicking, but want to stop them from swiping, then use ClickOnly
+ */
+ public enum MotionType {
+ AllowAll, ClickOnly, SwipeOnly
+ }
+ private Technique mTechnique;
+ private View mHighlightedView;
+ private Activity mActivity;
+ private MotionType mMotionType;
+ private FrameLayoutWithHole mFrameLayout;
+ private View mToolTipViewGroup;
+ public ToolTip mToolTip;
+ public Pointer mPointer;
+ public Overlay mOverlay;
+
+ private Sequence mSequence;
+
+ /*************
+ *
+ * Public API
+ *
+ *************/
+
+ /* Static builder */
+ public static TourGuide init(Activity activity){
+ return new TourGuide(activity);
+ }
+
+ /* Constructor */
+ public TourGuide(Activity activity){
+ mActivity = activity;
+ }
+
+ /**
+ * Setter for the animation to be used
+ * @param technique Animation to be used
+ * @return return TourGuide instance for chaining purpose
+ */
+ public TourGuide with(Technique technique) {
+ mTechnique = technique;
+ return this;
+ }
+
+ /**
+ * Sets which motion type is motionType
+ * @param motionType
+ * @return return TourGuide instance for chaining purpose
+ */
+ public TourGuide motionType(MotionType motionType){
+ mMotionType = motionType;
+ return this;
+ }
+
+ /**
+ * Sets the duration
+ * @param view the view in which the tutorial button will be placed on top of
+ * @return return TourGuide instance for chaining purpose
+ */
+ public TourGuide playOn(View view){
+ mHighlightedView = view;
+ setupView();
+ return this;
+ }
+
+ /**
+ * Sets the overlay
+ * @param overlay this overlay object should contain the attributes of the overlay, such as background color, animation, Style, etc
+ * @return return TourGuide instance for chaining purpose
+ */
+ public TourGuide setOverlay(Overlay overlay){
+ mOverlay = overlay;
+ return this;
+ }
+ /**
+ * Set the toolTip
+ * @param toolTip this toolTip object should contain the attributes of the ToolTip, such as, the title text, and the description text, background color, etc
+ * @return return TourGuide instance for chaining purpose
+ */
+ public TourGuide setToolTip(ToolTip toolTip){
+ mToolTip = toolTip;
+ return this;
+ }
+ /**
+ * Set the Pointer
+ * @param pointer this pointer object should contain the attributes of the Pointer, such as the pointer color, pointer gravity, etc, refer to @Link{pointer}
+ * @return return TourGuide instance for chaining purpose
+ */
+ public TourGuide setPointer(Pointer pointer){
+ mPointer = pointer;
+ return this;
+ }
+ /**
+ * Clean up the tutorial that is added to the activity
+ */
+ public void cleanUp(){
+ mFrameLayout.cleanUp();
+ if (mToolTipViewGroup!=null) {
+ ((ViewGroup) mActivity.getWindow().getDecorView()).removeView(mToolTipViewGroup);
+ }
+ }
+
+ public TourGuide playLater(View view){
+ mHighlightedView = view;
+ return this;
+ }
+
+ /**************************
+ * Sequence related method
+ **************************/
+
+ public TourGuide playInSequence(Sequence sequence){
+ setSequence(sequence);
+ next();
+ return this;
+ }
+
+ public TourGuide setSequence(Sequence sequence){
+ mSequence = sequence;
+ mSequence.setParentTourGuide(this);
+ for (TourGuide tourGuide : sequence.mTourGuideArray){
+ if (tourGuide.mHighlightedView == null) {
+ throw new NullPointerException("Please specify the view using 'playLater' method");
+ }
+ }
+ return this;
+ }
+
+ public TourGuide next(){
+ if (mFrameLayout!=null) {
+ cleanUp();
+ }
+
+ if (mSequence.mCurrentSequence < mSequence.mTourGuideArray.length) {
+ setToolTip(mSequence.getToolTip());
+ setPointer(mSequence.getPointer());
+ setOverlay(mSequence.getOverlay());
+
+ mHighlightedView = mSequence.getNextTourGuide().mHighlightedView;
+
+ setupView();
+ mSequence.mCurrentSequence++;
+ }
+ return this;
+ }
+
+ /**
+ *
+ * @return FrameLayoutWithHole that is used as overlay
+ */
+ public FrameLayoutWithHole getOverlay(){
+ return mFrameLayout;
+ }
+ /**
+ *
+ * @return the ToolTip container View
+ */
+ public View getToolTip(){
+ return mToolTipViewGroup;
+ }
+ /******
+ *
+ * Private methods
+ *
+ *******/
+ //TODO: move into Pointer
+ private int getXBasedOnGravity(int width){
+ int [] pos = new int[2];
+ mHighlightedView.getLocationOnScreen(pos);
+ int x = pos[0];
+ if((mPointer.mGravity & Gravity.RIGHT) == Gravity.RIGHT){
+ return x+mHighlightedView.getWidth()-width;
+ } else if ((mPointer.mGravity & Gravity.LEFT) == Gravity.LEFT) {
+ return x;
+ } else { // this is center
+ return x+mHighlightedView.getWidth()/2-width/2;
+ }
+ }
+ //TODO: move into Pointer
+ private int getYBasedOnGravity(int height){
+ int [] pos = new int[2];
+ mHighlightedView.getLocationInWindow(pos);
+ int y = pos[1];
+ if((mPointer.mGravity & Gravity.BOTTOM) == Gravity.BOTTOM){
+ return y+mHighlightedView.getHeight()-height;
+ } else if ((mPointer.mGravity & Gravity.TOP) == Gravity.TOP) {
+ return y;
+ }else { // this is center
+ return y+mHighlightedView.getHeight()/2-height/2;
+ }
+ }
+
+ private void setupView(){
+// TODO: throw exception if either mActivity, mDuration, mHighlightedView is null
+ checking();
+ final ViewTreeObserver viewTreeObserver = mHighlightedView.getViewTreeObserver();
+ viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ // make sure this only run once
+ mHighlightedView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+
+ /* Initialize a frame layout with a hole */
+ mFrameLayout = new FrameLayoutWithHole(mActivity, mHighlightedView, mMotionType, mOverlay);
+ /* handle click disable */
+ handleDisableClicking(mFrameLayout);
+
+ /* setup floating action button */
+ if (mPointer != null) {
+ FloatingActionButton fab = setupAndAddFABToFrameLayout(mFrameLayout);
+ performAnimationOn(fab);
+ }
+ setupFrameLayout();
+ /* setup tooltip view */
+ setupToolTip();
+ }
+ });
+ }
+ private void checking(){
+ // There is not check for tooltip because tooltip can be null, it means there no tooltip will be shown
+
+ }
+ private void handleDisableClicking(FrameLayoutWithHole frameLayoutWithHole){
+ // 1. if user provides an overlay listener, use that as 1st priority
+ if (mOverlay != null && mOverlay.mOnClickListener!=null) {
+ frameLayoutWithHole.setClickable(true);
+ frameLayoutWithHole.setOnClickListener(mOverlay.mOnClickListener);
+ }
+ // 2. if overlay listener is not provided, check if it's disabled
+ else if (mOverlay != null && mOverlay.mDisableClick) {
+ Log.w("tourguide", "Overlay's default OnClickListener is null, it will proceed to next tourguide when it is clicked");
+ frameLayoutWithHole.setViewHole(mHighlightedView);
+ frameLayoutWithHole.setSoundEffectsEnabled(false);
+ frameLayoutWithHole.setOnClickListener(new View.OnClickListener() {
+ @Override public void onClick(View v) {} // do nothing, disabled.
+ });
+ }
+ }
+
+ private void setupToolTip(){
+ final FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
+
+ if (mToolTip != null) {
+ /* inflate and get views */
+ ViewGroup parent = (ViewGroup) mActivity.getWindow().getDecorView();
+ LayoutInflater layoutInflater = mActivity.getLayoutInflater();
+ mToolTipViewGroup = layoutInflater.inflate(R.layout.tooltip, null);
+ View toolTipContainer = mToolTipViewGroup.findViewById(R.id.toolTip_container);
+ TextView toolTipTitleTV = (TextView) mToolTipViewGroup.findViewById(R.id.title);
+ TextView toolTipDescriptionTV = (TextView) mToolTipViewGroup.findViewById(R.id.description);
+
+ /* set tooltip attributes */
+ toolTipContainer.setBackgroundColor(mToolTip.mBackgroundColor);
+ if (mToolTip.mTitle == null){
+ toolTipTitleTV.setVisibility(View.GONE);
+ } else {
+ toolTipTitleTV.setText(mToolTip.mTitle);
+ }
+ if (mToolTip.mDescription == null){
+ toolTipDescriptionTV.setVisibility(View.GONE);
+ } else {
+ toolTipDescriptionTV.setText(mToolTip.mDescription);
+ }
+
+
+ mToolTipViewGroup.startAnimation(mToolTip.mEnterAnimation);
+
+ /* add setShadow if it's turned on */
+ if (mToolTip.mShadow) {
+ mToolTipViewGroup.setBackgroundDrawable(mActivity.getResources().getDrawable(R.drawable.drop_shadow));
+ }
+
+ /* position and size calculation */
+ int [] pos = new int[2];
+ mHighlightedView.getLocationOnScreen(pos);
+ int targetViewX = pos[0];
+ final int targetViewY = pos[1];
+
+ // get measured size of tooltip
+ mToolTipViewGroup.measure(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
+ int toolTipMeasuredWidth = mToolTipViewGroup.getMeasuredWidth();
+ int toolTipMeasuredHeight = mToolTipViewGroup.getMeasuredHeight();
+
+ Point resultPoint = new Point(); // this holds the final position of tooltip
+ float density = mActivity.getResources().getDisplayMetrics().density;
+ final float adjustment = 10 * density; //adjustment is that little overlapping area of tooltip and targeted button
+
+ // calculate x position, based on gravity, tooltipMeasuredWidth, parent max width, x position of target view, adjustment
+ if (toolTipMeasuredWidth > parent.getWidth()){
+ resultPoint.x = getXForTooTip(mToolTip.mGravity, parent.getWidth(), targetViewX, adjustment);
+ } else {
+ resultPoint.x = getXForTooTip(mToolTip.mGravity, toolTipMeasuredWidth, targetViewX, adjustment);
+ }
+
+ resultPoint.y = getYForTooTip(mToolTip.mGravity, toolTipMeasuredHeight, targetViewY, adjustment);
+
+ // add view to parent
+// ((ViewGroup) mActivity.getWindow().getDecorView().findViewById(android.R.id.content)).addView(mToolTipViewGroup, layoutParams);
+ parent.addView(mToolTipViewGroup, layoutParams);
+
+ // 1. width < screen check
+ if (toolTipMeasuredWidth > parent.getWidth()){
+ mToolTipViewGroup.getLayoutParams().width = parent.getWidth();
+ toolTipMeasuredWidth = parent.getWidth();
+ }
+ // 2. x left boundary check
+ if (resultPoint.x < 0){
+ mToolTipViewGroup.getLayoutParams().width = toolTipMeasuredWidth + resultPoint.x; //since point.x is negative, use plus
+ resultPoint.x = 0;
+ }
+ // 3. x right boundary check
+ int tempRightX = resultPoint.x + toolTipMeasuredWidth;
+ if ( tempRightX > parent.getWidth()){
+ mToolTipViewGroup.getLayoutParams().width = parent.getWidth() - resultPoint.x; //since point.x is negative, use plus
+ }
+
+ // pass toolTip onClickListener into toolTipViewGroup
+ if (mToolTip.mOnClickListener!=null) {
+ mToolTipViewGroup.setOnClickListener(mToolTip.mOnClickListener);
+ }
+
+ // TODO: no boundary check for height yet, this is a unlikely case though
+ // height boundary can be fixed by user changing the gravity to the other size, since there are plenty of space vertically compared to horizontally
+
+ // this needs an viewTreeObserver, that's because TextView measurement of it's vertical height is not accurate (didn't take into account of multiple lines yet) before it's rendered
+ // re-calculate height again once it's rendered
+ final ViewTreeObserver viewTreeObserver = mToolTipViewGroup.getViewTreeObserver();
+ viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mToolTipViewGroup.getViewTreeObserver().removeGlobalOnLayoutListener(this);// make sure this only run once
+
+ int fixedY;
+ int toolTipHeightAfterLayouted = mToolTipViewGroup.getHeight();
+ fixedY = getYForTooTip(mToolTip.mGravity, toolTipHeightAfterLayouted, targetViewY, adjustment);
+ layoutParams.setMargins((int)mToolTipViewGroup.getX(),fixedY,0,0);
+ }
+ });
+
+ // set the position using setMargins on the left and top
+ layoutParams.setMargins(resultPoint.x, resultPoint.y, 0, 0);
+ }
+
+ }
+
+ private int getXForTooTip(int gravity, int toolTipMeasuredWidth, int targetViewX, float adjustment){
+ int x;
+ if ((gravity & Gravity.LEFT) == Gravity.LEFT){
+ x = targetViewX - toolTipMeasuredWidth + (int)adjustment;
+ } else if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) {
+ x = targetViewX + mHighlightedView.getWidth() - (int)adjustment;
+ } else {
+ x = targetViewX + mHighlightedView.getWidth() / 2 - toolTipMeasuredWidth / 2;
+ }
+ return x;
+ }
+ private int getYForTooTip(int gravity, int toolTipMeasuredHeight, int targetViewY, float adjustment){
+ int y;
+ if ((gravity & Gravity.TOP) == Gravity.TOP) {
+
+ if (((gravity & Gravity.LEFT) == Gravity.LEFT) || ((gravity & Gravity.RIGHT) == Gravity.RIGHT)) {
+ y = targetViewY - toolTipMeasuredHeight + (int)adjustment;
+ } else {
+ y = targetViewY - toolTipMeasuredHeight - (int)adjustment;
+ }
+ } else if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) {
+ if (((gravity & Gravity.LEFT) == Gravity.LEFT) || ((gravity & Gravity.RIGHT) == Gravity.RIGHT)) {
+ y = targetViewY + mHighlightedView.getHeight() - (int) adjustment;
+ } else {
+ y = targetViewY + mHighlightedView.getHeight() + (int) adjustment;
+ }
+ } else { // this is center
+ if (((gravity & Gravity.LEFT) == Gravity.LEFT) || ((gravity & Gravity.RIGHT) == Gravity.RIGHT)) {
+ y = targetViewY + mHighlightedView.getHeight() / 2 - (int) adjustment;
+ } else {
+ y = targetViewY + mHighlightedView.getHeight() / 2 + (int) adjustment;
+ }
+ }
+ return y;
+ }
+
+ private FloatingActionButton setupAndAddFABToFrameLayout(final FrameLayoutWithHole frameLayoutWithHole){
+ // invisFab is invisible, and it's only used for getting the width and height
+ final FloatingActionButton invisFab = new FloatingActionButton(mActivity);
+ invisFab.setSize(FloatingActionButton.SIZE_MINI);
+ invisFab.setVisibility(View.INVISIBLE);
+ ((ViewGroup)mActivity.getWindow().getDecorView()).addView(invisFab);
+
+ // fab is the real fab that is going to be added
+ final FloatingActionButton fab = new FloatingActionButton(mActivity);
+ fab.setBackgroundColor(Color.BLUE);
+ fab.setSize(FloatingActionButton.SIZE_MINI);
+ fab.setColorNormal(mPointer.mColor);
+ fab.setStrokeVisible(false);
+ fab.setClickable(false);
+
+ // When invisFab is layouted, it's width and height can be used to calculate the correct position of fab
+ final ViewTreeObserver viewTreeObserver = invisFab.getViewTreeObserver();
+ viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ // make sure this only run once
+ invisFab.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ frameLayoutWithHole.addView(fab, params);
+
+ // measure size of image to be placed
+ params.setMargins(getXBasedOnGravity(invisFab.getWidth()), getYBasedOnGravity(invisFab.getHeight()), 0, 0);
+ }
+ });
+
+
+ return fab;
+ }
+
+ private void setupFrameLayout(){
+ FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
+ ViewGroup contentArea = (ViewGroup) mActivity.getWindow().getDecorView().findViewById(android.R.id.content);
+ int [] pos = new int[2];
+ contentArea.getLocationOnScreen(pos);
+ // frameLayoutWithHole's coordinates are calculated taking full screen height into account
+ // but we're adding it to the content area only, so we need to offset it to the same Y value of contentArea
+
+ layoutParams.setMargins(0,-pos[1],0,0);
+ contentArea.addView(mFrameLayout, layoutParams);
+ }
+
+ private void performAnimationOn(final View view){
+
+ if (mTechnique != null && mTechnique == Technique.HorizontalLeft){
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ final AnimatorSet animatorSet2 = new AnimatorSet();
+ Animator.AnimatorListener lis1 = new Animator.AnimatorListener() {
+ @Override public void onAnimationStart(Animator animator) {}
+ @Override public void onAnimationCancel(Animator animator) {}
+ @Override public void onAnimationRepeat(Animator animator) {}
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ view.setScaleX(1f);
+ view.setScaleY(1f);
+ view.setTranslationX(0);
+ animatorSet2.start();
+ }
+ };
+ Animator.AnimatorListener lis2 = new Animator.AnimatorListener() {
+ @Override public void onAnimationStart(Animator animator) {}
+ @Override public void onAnimationCancel(Animator animator) {}
+ @Override public void onAnimationRepeat(Animator animator) {}
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ view.setScaleX(1f);
+ view.setScaleY(1f);
+ view.setTranslationX(0);
+ animatorSet.start();
+ }
+ };
+
+ long fadeInDuration = 800;
+ long scaleDownDuration = 800;
+ long goLeftXDuration = 2000;
+ long fadeOutDuration = goLeftXDuration;
+ float translationX = getScreenWidth()/2;
+
+ final ValueAnimator fadeInAnim = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
+ fadeInAnim.setDuration(fadeInDuration);
+ final ObjectAnimator scaleDownX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.85f);
+ scaleDownX.setDuration(scaleDownDuration);
+ final ObjectAnimator scaleDownY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.85f);
+ scaleDownY.setDuration(scaleDownDuration);
+ final ObjectAnimator goLeftX = ObjectAnimator.ofFloat(view, "translationX", -translationX);
+ goLeftX.setDuration(goLeftXDuration);
+ final ValueAnimator fadeOutAnim = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);
+ fadeOutAnim.setDuration(fadeOutDuration);
+
+ final ValueAnimator fadeInAnim2 = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
+ fadeInAnim2.setDuration(fadeInDuration);
+ final ObjectAnimator scaleDownX2 = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.85f);
+ scaleDownX2.setDuration(scaleDownDuration);
+ final ObjectAnimator scaleDownY2 = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.85f);
+ scaleDownY2.setDuration(scaleDownDuration);
+ final ObjectAnimator goLeftX2 = ObjectAnimator.ofFloat(view, "translationX", -translationX);
+ goLeftX2.setDuration(goLeftXDuration);
+ final ValueAnimator fadeOutAnim2 = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);
+ fadeOutAnim2.setDuration(fadeOutDuration);
+
+ animatorSet.play(fadeInAnim);
+ animatorSet.play(scaleDownX).with(scaleDownY).after(fadeInAnim);
+ animatorSet.play(goLeftX).with(fadeOutAnim).after(scaleDownY);
+
+ animatorSet2.play(fadeInAnim2);
+ animatorSet2.play(scaleDownX2).with(scaleDownY2).after(fadeInAnim2);
+ animatorSet2.play(goLeftX2).with(fadeOutAnim2).after(scaleDownY2);
+
+ animatorSet.addListener(lis1);
+ animatorSet2.addListener(lis2);
+ animatorSet.start();
+
+ /* these animatorSets are kept track in FrameLayout, so that they can be cleaned up when FrameLayout is detached from window */
+ mFrameLayout.addAnimatorSet(animatorSet);
+ mFrameLayout.addAnimatorSet(animatorSet2);
+ } else if (mTechnique != null && mTechnique == Technique.HorizontalRight){
+
+ } else if (mTechnique != null && mTechnique == Technique.VerticalUpward){
+
+ } else if (mTechnique != null && mTechnique == Technique.VerticalDownward){
+
+ } else { // do click for default case
+ final AnimatorSet animatorSet = new AnimatorSet();
+ final AnimatorSet animatorSet2 = new AnimatorSet();
+ Animator.AnimatorListener lis1 = new Animator.AnimatorListener() {
+ @Override public void onAnimationStart(Animator animator) {}
+ @Override public void onAnimationCancel(Animator animator) {}
+ @Override public void onAnimationRepeat(Animator animator) {}
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ view.setScaleX(1f);
+ view.setScaleY(1f);
+ view.setTranslationX(0);
+ animatorSet2.start();
+ }
+ };
+ Animator.AnimatorListener lis2 = new Animator.AnimatorListener() {
+ @Override public void onAnimationStart(Animator animator) {}
+ @Override public void onAnimationCancel(Animator animator) {}
+ @Override public void onAnimationRepeat(Animator animator) {}
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ view.setScaleX(1f);
+ view.setScaleY(1f);
+ view.setTranslationX(0);
+ animatorSet.start();
+ }
+ };
+
+ long fadeInDuration = 800;
+ long scaleDownDuration = 800;
+ long fadeOutDuration = 800;
+ long delay = 1000;
+
+ final ValueAnimator delayAnim = ObjectAnimator.ofFloat(view, "translationX", 0);
+ delayAnim.setDuration(delay);
+ final ValueAnimator fadeInAnim = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
+ fadeInAnim.setDuration(fadeInDuration);
+ final ObjectAnimator scaleDownX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.85f);
+ scaleDownX.setDuration(scaleDownDuration);
+ final ObjectAnimator scaleDownY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.85f);
+ scaleDownY.setDuration(scaleDownDuration);
+ final ObjectAnimator scaleUpX = ObjectAnimator.ofFloat(view, "scaleX", 0.85f, 1f);
+ scaleUpX.setDuration(scaleDownDuration);
+ final ObjectAnimator scaleUpY = ObjectAnimator.ofFloat(view, "scaleY", 0.85f, 1f);
+ scaleUpY.setDuration(scaleDownDuration);
+ final ValueAnimator fadeOutAnim = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);
+ fadeOutAnim.setDuration(fadeOutDuration);
+
+ final ValueAnimator delayAnim2 = ObjectAnimator.ofFloat(view, "translationX", 0);
+ delayAnim2.setDuration(delay);
+ final ValueAnimator fadeInAnim2 = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
+ fadeInAnim2.setDuration(fadeInDuration);
+ final ObjectAnimator scaleDownX2 = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.85f);
+ scaleDownX2.setDuration(scaleDownDuration);
+ final ObjectAnimator scaleDownY2 = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.85f);
+ scaleDownY2.setDuration(scaleDownDuration);
+ final ObjectAnimator scaleUpX2 = ObjectAnimator.ofFloat(view, "scaleX", 0.85f, 1f);
+ scaleUpX2.setDuration(scaleDownDuration);
+ final ObjectAnimator scaleUpY2 = ObjectAnimator.ofFloat(view, "scaleY", 0.85f, 1f);
+ scaleUpY2.setDuration(scaleDownDuration);
+ final ValueAnimator fadeOutAnim2 = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);
+ fadeOutAnim2.setDuration(fadeOutDuration);
+ view.setAlpha(0);
+ animatorSet.setStartDelay(mToolTip != null ? mToolTip.mEnterAnimation.getDuration() : 0);
+ animatorSet.play(fadeInAnim);
+ animatorSet.play(scaleDownX).with(scaleDownY).after(fadeInAnim);
+ animatorSet.play(scaleUpX).with(scaleUpY).with(fadeOutAnim).after(scaleDownY);
+ animatorSet.play(delayAnim).after(scaleUpY);
+
+ animatorSet2.play(fadeInAnim2);
+ animatorSet2.play(scaleDownX2).with(scaleDownY2).after(fadeInAnim2);
+ animatorSet2.play(scaleUpX2).with(scaleUpY2).with(fadeOutAnim2).after(scaleDownY2);
+ animatorSet2.play(delayAnim2).after(scaleUpY2);
+
+ animatorSet.addListener(lis1);
+ animatorSet2.addListener(lis2);
+ animatorSet.start();
+
+ /* these animatorSets are kept track in FrameLayout, so that they can be cleaned up when FrameLayout is detached from window */
+ mFrameLayout.addAnimatorSet(animatorSet);
+ mFrameLayout.addAnimatorSet(animatorSet2);
+ }
+ }
+ private int getScreenWidth(){
+ if (mActivity!=null) {
+ return mActivity.getResources().getDisplayMetrics().widthPixels;
+ } else {
+ return 0;
+ }
+ }
+
+}