From cde2ea4cf15176b937137da58f854d86f301b835 Mon Sep 17 00:00:00 2001 From: Dominik Mosberger Date: Fri, 8 Jul 2016 09:32:46 +0200 Subject: [PATCH 1/3] Added animation for Technique.HorizontalRight --- .../java/tourguide/tourguide/TourGuide.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tourguide/src/main/java/tourguide/tourguide/TourGuide.java b/tourguide/src/main/java/tourguide/tourguide/TourGuide.java index d0e255a..cd42f6d 100644 --- a/tourguide/src/main/java/tourguide/tourguide/TourGuide.java +++ b/tourguide/src/main/java/tourguide/tourguide/TourGuide.java @@ -436,8 +436,7 @@ private void setupFrameLayout(){ private void performAnimationOn(final View view){ - if (mTechnique != null && mTechnique == Technique.HorizontalLeft){ - + if (mTechnique != null && (mTechnique == Technique.HorizontalLeft || mTechnique == Technique.HorizontalRight)){ final AnimatorSet animatorSet = new AnimatorSet(); final AnimatorSet animatorSet2 = new AnimatorSet(); Animator.AnimatorListener lis1 = new Animator.AnimatorListener() { @@ -467,9 +466,11 @@ public void onAnimationEnd(Animator animator) { long fadeInDuration = 800; long scaleDownDuration = 800; - long goLeftXDuration = 2000; - long fadeOutDuration = goLeftXDuration; + long horizontalMoveDuration = 2000; float translationX = getScreenWidth()/2; + if (mTechnique == Technique.HorizontalLeft) { + translationX = -translationX; + } final ValueAnimator fadeInAnim = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f); fadeInAnim.setDuration(fadeInDuration); @@ -477,10 +478,10 @@ public void onAnimationEnd(Animator animator) { 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 ObjectAnimator goLeftX = ObjectAnimator.ofFloat(view, "translationX", translationX); + goLeftX.setDuration(horizontalMoveDuration); final ValueAnimator fadeOutAnim = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f); - fadeOutAnim.setDuration(fadeOutDuration); + fadeOutAnim.setDuration(horizontalMoveDuration); final ValueAnimator fadeInAnim2 = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f); fadeInAnim2.setDuration(fadeInDuration); @@ -488,10 +489,10 @@ public void onAnimationEnd(Animator animator) { 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 ObjectAnimator goLeftX2 = ObjectAnimator.ofFloat(view, "translationX", translationX); + goLeftX2.setDuration(horizontalMoveDuration); final ValueAnimator fadeOutAnim2 = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f); - fadeOutAnim2.setDuration(fadeOutDuration); + fadeOutAnim2.setDuration(horizontalMoveDuration); animatorSet.play(fadeInAnim); animatorSet.play(scaleDownX).with(scaleDownY).after(fadeInAnim); @@ -508,8 +509,6 @@ public void onAnimationEnd(Animator animator) { /* 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){ From 245ba1e65558f374bdcb8408dc20776d0ae1efaa Mon Sep 17 00:00:00 2001 From: Dominik Mosberger Date: Wed, 13 Jul 2016 11:47:13 +0200 Subject: [PATCH 2/3] Updated Support Library, used default Floating Action Button --- app/build.gradle | 13 +- tourguide/build.gradle | 8 +- .../tourguide/FrameLayoutWithHole.java | 13 +- .../java/tourguide/tourguide/Overlay.java | 50 +- .../java/tourguide/tourguide/Pointer.java | 11 + .../java/tourguide/tourguide/TourGuide.java | 462 ++++++++++-------- 6 files changed, 330 insertions(+), 227 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cb85201..761e217 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,8 +21,8 @@ Properties getGradleProperties(){ } android { - compileSdkVersion 21 - buildToolsVersion "21.1.2" + compileSdkVersion 23 + buildToolsVersion "23.0.1" def gradleProps = getGradleProperties() defaultConfig { applicationId "tourguide.tourguide" @@ -49,9 +49,8 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:22.2.0' - /* For navigation view (drawer) */ - compile 'com.android.support:design:22.2.0' + compile 'com.android.support:appcompat-v7:24.0.0' + compile 'com.android.support:design:24.0.0' compile project(':tourguide') // compile ('com.github.worker8:tourguide:1.0.16-SNAPSHOT@aar'){ // transitive=true @@ -62,7 +61,9 @@ dependencies { releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // Set this dependency to build and run Espresso tests - androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2' + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2') { + exclude group: 'com.android.support', module: 'support-annotations' + } } task print_git_tag_command << { diff --git a/tourguide/build.gradle b/tourguide/build.gradle index a32a42e..0a230e3 100644 --- a/tourguide/build.gradle +++ b/tourguide/build.gradle @@ -15,8 +15,8 @@ Properties getGradleProperties(){ } android { - compileSdkVersion 21 - buildToolsVersion "21.1.2" + compileSdkVersion 23 + buildToolsVersion "23.0.1" def gradleProps = getGradleProperties() defaultConfig { minSdkVersion 11 @@ -37,8 +37,8 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:22.2.0' - compile 'net.i2p.android.ext:floatingactionbutton:1.9.0' + compile 'com.android.support:appcompat-v7:24.0.0' + compile 'com.android.support:design:24.0.0' } apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle' \ No newline at end of file diff --git a/tourguide/src/main/java/tourguide/tourguide/FrameLayoutWithHole.java b/tourguide/src/main/java/tourguide/tourguide/FrameLayoutWithHole.java index 1f18633..b2d67fd 100644 --- a/tourguide/src/main/java/tourguide/tourguide/FrameLayoutWithHole.java +++ b/tourguide/src/main/java/tourguide/tourguide/FrameLayoutWithHole.java @@ -9,6 +9,7 @@ import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; import android.text.TextPaint; import android.util.AttributeSet; import android.util.Log; @@ -257,7 +258,7 @@ protected void onDraw(Canvas canvas) { if (mOverlay!=null) { mEraserCanvas.drawColor(mOverlay.mBackgroundColor); - int padding = (int) (10 * mDensity); + int padding = (int) (mOverlay.mHolePadding * mDensity); if (mOverlay.mStyle == Overlay.Style.Rectangle) { mEraserCanvas.drawRect( @@ -309,4 +310,14 @@ public int getScreenWidth(Activity activity){ public int getScreenHeight(Activity activity){ return activity.getResources().getDisplayMetrics().heightPixels; } + + public Rect getHighlightedView() { + int[] position = new int[2]; + mViewHole.getLocationOnScreen(position); + int left = position[0]; + int right = left + mViewHole.getWidth(); + int top = position[1]; + int bottom = top + mViewHole.getHeight(); + return new Rect(left, top, right, bottom); + } } diff --git a/tourguide/src/main/java/tourguide/tourguide/Overlay.java b/tourguide/src/main/java/tourguide/tourguide/Overlay.java index 9b1f5ab..a16603a 100644 --- a/tourguide/src/main/java/tourguide/tourguide/Overlay.java +++ b/tourguide/src/main/java/tourguide/tourguide/Overlay.java @@ -8,6 +8,7 @@ * {@link Overlay} shows a tinted background to cover up the rest of the screen. A 'hole' will be made on this overlay to let users obtain focus on the targeted element. */ public class Overlay { + public final static int NOT_SET = -1; public int mBackgroundColor; public boolean mDisableClick; public boolean mDisableClickThroughHole; @@ -17,11 +18,8 @@ public class Overlay { public int mHoleOffsetTop = 0; public View.OnClickListener mOnClickListener; public int mHoleRadius = NOT_SET; - public final static int NOT_SET = -1; - - public enum Style { - Circle, Rectangle, NoHole - } + public int mHolePadding = 20; +// public int public Overlay() { this(true, Color.parseColor("#55000000"), Style.Circle); @@ -35,20 +33,22 @@ public Overlay(boolean disableClick, int backgroundColor, Style style) { /** * Set background color + * * @param backgroundColor * @return return {@link Overlay} instance for chaining purpose */ - public Overlay setBackgroundColor(int backgroundColor){ + 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 yesNo * @return return {@link Overlay} instance for chaining purpose */ - public Overlay disableClick(boolean yesNo){ + public Overlay disableClick(boolean yesNo) { mDisableClick = yesNo; return this; } @@ -56,45 +56,50 @@ public Overlay disableClick(boolean yesNo){ /** * Set to true if you want to disallow the highlighted view to be clicked through the hole, * set to false if you want to allow the highlighted view to be clicked through the hole + * * @param yesNo * @return return Overlay instance for chaining purpose */ - public Overlay disableClickThroughHole(boolean yesNo){ + public Overlay disableClickThroughHole(boolean yesNo) { mDisableClickThroughHole = yesNo; return this; } - public Overlay setStyle(Style style){ + public Overlay setStyle(Style style) { mStyle = style; return this; } /** * Set enter animation + * * @param enterAnimation * @return return {@link Overlay} instance for chaining purpose */ - public Overlay setEnterAnimation(Animation enterAnimation){ + public Overlay setEnterAnimation(Animation enterAnimation) { mEnterAnimation = enterAnimation; return this; } + /** * Set exit animation + * * @param exitAnimation * @return return {@link Overlay} instance for chaining purpose */ - public Overlay setExitAnimation(Animation exitAnimation){ + public Overlay setExitAnimation(Animation exitAnimation) { mExitAnimation = exitAnimation; return this; } /** * Set {@link Overlay#mOnClickListener} for the {@link Overlay} + * * @param onClickListener * @return return {@link Overlay} instance for chaining purpose */ - public Overlay setOnClickListener(View.OnClickListener onClickListener){ - mOnClickListener=onClickListener; + public Overlay setOnClickListener(View.OnClickListener onClickListener) { + mOnClickListener = onClickListener; return this; } @@ -103,6 +108,7 @@ public Overlay setOnClickListener(View.OnClickListener onClickListener){ * If this is not set, the size of view hole fill follow the max(view.width, view.height) * If this is set, it will take precedence * It only has effect when {@link Overlay.Style#Circle} is chosen + * * @param holeRadius the radius of the view hole, setting 0 will make the hole disappear, in pixels * @return return {@link Overlay} instance for chaining purpose */ @@ -111,11 +117,11 @@ public Overlay setHoleRadius(int holeRadius) { return this; } - /** * This method sets offsets to the hole's position relative the position of the targeted view. + * * @param offsetLeft left offset, in pixels - * @param offsetTop top offset, in pixels + * @param offsetTop top offset, in pixels * @return {@link Overlay} instance for chaining purpose */ public Overlay setHoleOffsets(int offsetLeft, int offsetTop) { @@ -123,4 +129,18 @@ public Overlay setHoleOffsets(int offsetLeft, int offsetTop) { mHoleOffsetTop = offsetTop; return this; } + + /** + * This method sets the padding of a rectangular hole. + * @param holePadding padding in dp + * @return {@link Overlay} instance for chaining purpose + */ + public Overlay setHolePadding(int holePadding) { + mHolePadding = holePadding; + return this; + } + + public enum Style { + Circle, Rectangle, NoHole + } } diff --git a/tourguide/src/main/java/tourguide/tourguide/Pointer.java b/tourguide/src/main/java/tourguide/tourguide/Pointer.java index c820277..fa639b6 100644 --- a/tourguide/src/main/java/tourguide/tourguide/Pointer.java +++ b/tourguide/src/main/java/tourguide/tourguide/Pointer.java @@ -9,6 +9,7 @@ public class Pointer { public int mGravity = Gravity.CENTER; public int mColor = Color.WHITE; + public int mColorPressed = Color.BLUE; public Pointer() { this(Gravity.CENTER, Color.parseColor("#FFFFFF")); @@ -38,4 +39,14 @@ public Pointer setGravity(int gravity){ mGravity = gravity; return this; } + + /** + * Set colorPressed + * @param colorPressed + * @return return Pointer instance for chaining purpose + */ + public Pointer setColorPressed(int colorPressed) { + mColorPressed = colorPressed; + return this; + } } diff --git a/tourguide/src/main/java/tourguide/tourguide/TourGuide.java b/tourguide/src/main/java/tourguide/tourguide/TourGuide.java index cd42f6d..e1336fb 100644 --- a/tourguide/src/main/java/tourguide/tourguide/TourGuide.java +++ b/tourguide/src/main/java/tourguide/tourguide/TourGuide.java @@ -5,10 +5,11 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.Activity; -import android.graphics.Color; import android.graphics.Point; -import android.support.v4.view.ViewCompat; +import android.graphics.Rect; import android.os.Build; +import android.support.design.widget.FloatingActionButton; +import android.support.v4.view.ViewCompat; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; @@ -19,53 +20,43 @@ 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 - } + public ToolTip mToolTip; + public Pointer mPointer; + public Overlay mOverlay; protected Technique mTechnique; protected View mHighlightedView; - private Activity mActivity; protected MotionType mMotionType; protected FrameLayoutWithHole mFrameLayout; + private Activity mActivity; private View mToolTipViewGroup; - public ToolTip mToolTip; - public Pointer mPointer; - public Overlay mOverlay; + private boolean mUseDefaultMeasurement; + private int mMaxBottom; + + /* Constructor */ + public TourGuide(Activity activity) { + mActivity = activity; + } /************* - * * Public API - * *************/ /* Static builder */ - public static TourGuide init(Activity activity){ + public static TourGuide init(Activity activity) { return new TourGuide(activity); } - /* Constructor */ - public TourGuide(Activity activity){ - mActivity = activity; + public int getMaxBottom() { + return mMaxBottom; } /** * Setter for the animation to be used + * * @param technique Animation to be used * @return return TourGuide instance for chaining purpose */ @@ -76,114 +67,126 @@ public TourGuide with(Technique technique) { /** * Sets which motion type is motionType + * * @param motionType * @return return TourGuide instance for chaining purpose */ - public TourGuide motionType(MotionType motionType){ + public TourGuide motionType(MotionType motionType) { mMotionType = motionType; return this; } /** * Sets the targeted view for TourGuide to play on + * * @param targetView the view in which the tutorial button will be placed on top of * @return return TourGuide instance for chaining purpose */ - public TourGuide playOn(View targetView){ + public TourGuide playOn(View targetView) { mHighlightedView = targetView; 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){ + 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 void cleanUp() { + mFrameLayout.cleanUp(); + if (mToolTipViewGroup != null) { + ((ViewGroup) mActivity.getWindow().getDecorView()).removeView(mToolTipViewGroup); + } + } + + public TourGuide setUseDefaultMeasurement(boolean useDefaultMeasurement) { + mUseDefaultMeasurement = useDefaultMeasurement; + return this; } /** - * * @return FrameLayoutWithHole that is used as overlay */ - public FrameLayoutWithHole getOverlay(){ + public FrameLayoutWithHole getOverlay() { return mFrameLayout; } + /** + * 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; + } + + /** * @return the ToolTip container View */ - public View getToolTip(){ + public View getToolTip() { return mToolTipViewGroup; } - /****** + + /** + * 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; + } + + /****** * Private methods - * *******/ //TODO: move into Pointer - private int getXBasedOnGravity(int width){ - int [] pos = new int[2]; + 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; + 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; + return x + mHighlightedView.getWidth() / 2 - width / 2; } } + //TODO: move into Pointer - private int getYBasedOnGravity(int height){ - int [] pos = new int[2]; + 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; + 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; + } else { // this is center + return y + mHighlightedView.getHeight() / 2 - height / 2; } } - protected void setupView(){ + protected void setupView() { // TourGuide can only be setup after all the views is ready and obtain it's position/measurement // so when this is the 1st time TourGuide is being added, // else block will be executed, and ViewTreeObserver will make TourGuide setup process to be delayed until everything is ready // when this is run the 2nd or more times, if block will be executed - if (ViewCompat.isAttachedToWindow(mHighlightedView)){ + if (ViewCompat.isAttachedToWindow(mHighlightedView)) { startView(); } else { final ViewTreeObserver viewTreeObserver = mHighlightedView.getViewTreeObserver(); @@ -202,7 +205,7 @@ public void onGlobalLayout() { } } - private void startView(){ + private void startView() { /* Initialize a frame layout with a hole */ mFrameLayout = new FrameLayoutWithHole(mActivity, mHighlightedView, mMotionType, mOverlay); /* handle click disable */ @@ -218,9 +221,9 @@ private void startView(){ setupToolTip(); } - private void handleDisableClicking(FrameLayoutWithHole frameLayoutWithHole){ + private void handleDisableClicking(FrameLayoutWithHole frameLayoutWithHole) { // 1. if user provides an overlay listener, use that as 1st priority - if (mOverlay != null && mOverlay.mOnClickListener!=null) { + if (mOverlay != null && mOverlay.mOnClickListener != null) { frameLayoutWithHole.setClickable(true); frameLayoutWithHole.setOnClickListener(mOverlay.mOnClickListener); } @@ -230,17 +233,19 @@ else if (mOverlay != null && mOverlay.mDisableClick) { frameLayoutWithHole.setViewHole(mHighlightedView); frameLayoutWithHole.setSoundEffectsEnabled(false); frameLayoutWithHole.setOnClickListener(new View.OnClickListener() { - @Override public void onClick(View v) {} // do nothing, disabled. + @Override + public void onClick(View v) { + } // do nothing, disabled. }); } } - private void setupToolTip(){ + 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(); + ViewGroup parent = mFrameLayout; LayoutInflater layoutInflater = mActivity.getLayoutInflater(); if (mToolTip.getCustomView() == null) { @@ -276,173 +281,178 @@ private void setupToolTip(){ 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); + if (mUseDefaultMeasurement) { + Rect highlightedView = mFrameLayout.getHighlightedView(); + FrameLayout layout = new FrameLayout(mActivity); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(highlightedView.width(), getScreenHeight() - highlightedView.bottom); + layout.setLayoutParams(params); + layout.setX(highlightedView.left); + layout.setY(highlightedView.bottom); + layout.addView(mToolTipViewGroup); + + mMaxBottom = getScreenHeight() - highlightedView.bottom; + + parent.addView(layout, layoutParams); } else { - resultPoint.x = getXForTooTip(mToolTip.mGravity, toolTipMeasuredWidth, targetViewX, adjustment); - } + /* 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); + resultPoint.y = getYForTooTip(mToolTip.mGravity, toolTipMeasuredHeight, targetViewY, adjustment); - // add view to parent + // add view to parent // ((ViewGroup) mActivity.getWindow().getDecorView().findViewById(android.R.id.content)).addView(mToolTipViewGroup, layoutParams); - parent.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); - } + // 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 + } - // 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 + // pass toolTip onClickListener into toolTipViewGroup + if (mToolTip.mOnClickListener != null) { + mToolTipViewGroup.setOnClickListener(mToolTip.mOnClickListener); + } - // 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 - mToolTipViewGroup.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - // make sure this only run once - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { - //noinspection deprecation - mToolTipViewGroup.getViewTreeObserver().removeGlobalOnLayoutListener(this); - } else { - mToolTipViewGroup.getViewTreeObserver().removeOnGlobalLayoutListener(this); + // 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 + mToolTipViewGroup.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + // make sure this only run once + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + //noinspection deprecation + mToolTipViewGroup.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } else { + mToolTipViewGroup.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + + int fixedY; + int toolTipHeightAfterLayouted = mToolTipViewGroup.getHeight(); + fixedY = getYForTooTip(mToolTip.mGravity, toolTipHeightAfterLayouted, targetViewY, adjustment); + layoutParams.setMargins((int) mToolTipViewGroup.getX(), fixedY, 0, 0); } + }); - 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); + // 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){ + private int getXForTooTip(int gravity, int toolTipMeasuredWidth, int targetViewX, float adjustment) { int x; - if ((gravity & Gravity.LEFT) == Gravity.LEFT){ - x = targetViewX - toolTipMeasuredWidth + (int)adjustment; + if ((gravity & Gravity.LEFT) == Gravity.LEFT) { + x = targetViewX - toolTipMeasuredWidth + (int) adjustment; } else if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { - x = targetViewX + mHighlightedView.getWidth() - (int)adjustment; + 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){ + + 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; + y = targetViewY - toolTipMeasuredHeight + (int) adjustment; } else { - y = targetViewY - toolTipMeasuredHeight - (int)adjustment; + y = targetViewY - toolTipMeasuredHeight - (int) adjustment; } } else { // this is center if (((gravity & Gravity.LEFT) == Gravity.LEFT) || ((gravity & Gravity.RIGHT) == Gravity.RIGHT)) { - y = targetViewY + mHighlightedView.getHeight() - (int) adjustment; + y = targetViewY + mHighlightedView.getHeight() - (int) adjustment; } else { - y = targetViewY + mHighlightedView.getHeight() + (int) adjustment; + y = targetViewY + mHighlightedView.getHeight() + (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); - + private FloatingActionButton setupAndAddFABToFrameLayout(final FrameLayoutWithHole frameLayoutWithHole) { // fab is the real fab that is going to be added final FloatingActionButton fab = new FloatingActionButton(mActivity); - fab.setBackgroundColor(Color.BLUE); + fab.setRippleColor(mPointer.mColorPressed); fab.setSize(FloatingActionButton.SIZE_MINI); - fab.setColorNormal(mPointer.mColor); - fab.setStrokeVisible(false); + fab.setBackgroundColor(mPointer.mColor); fab.setClickable(false); - // When invisFab is layouted, it's width and height can be used to calculate the correct position of fab - invisFab.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - // make sure this only run once - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { - //noinspection deprecation - invisFab.getViewTreeObserver().removeGlobalOnLayoutListener(this); - } else { - invisFab.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } - final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); - frameLayoutWithHole.addView(fab, params); + int dimensionPixelSize = mActivity.getResources().getDimensionPixelSize(android.support.design.R.dimen.design_fab_size_mini); - // measure size of image to be placed - params.setMargins(getXBasedOnGravity(invisFab.getWidth()), getYBasedOnGravity(invisFab.getHeight()), 0, 0); - } - }); + 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(dimensionPixelSize), getYBasedOnGravity(dimensionPixelSize), 0, 0); return fab; } - private void setupFrameLayout(){ + 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]; + 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); + layoutParams.setMargins(0, -pos[1], 0, 0); contentArea.addView(mFrameLayout, layoutParams); } - private void performAnimationOn(final View view){ + private void performAnimationOn(final View view) { - if (mTechnique != null && (mTechnique == Technique.HorizontalLeft || mTechnique == Technique.HorizontalRight)){ + if (mTechnique != null && (mTechnique == Technique.HorizontalLeft || mTechnique == Technique.HorizontalRight)) { 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 onAnimationStart(Animator animator) { + } + + @Override + public void onAnimationCancel(Animator animator) { + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + @Override public void onAnimationEnd(Animator animator) { view.setScaleX(1f); @@ -452,9 +462,18 @@ public void onAnimationEnd(Animator animator) { } }; 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 onAnimationStart(Animator animator) { + } + + @Override + public void onAnimationCancel(Animator animator) { + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + @Override public void onAnimationEnd(Animator animator) { view.setScaleX(1f); @@ -467,7 +486,7 @@ public void onAnimationEnd(Animator animator) { long fadeInDuration = 800; long scaleDownDuration = 800; long horizontalMoveDuration = 2000; - float translationX = getScreenWidth()/2; + float translationX = getScreenWidth() / 2; if (mTechnique == Technique.HorizontalLeft) { translationX = -translationX; } @@ -509,17 +528,26 @@ public void onAnimationEnd(Animator animator) { /* 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.VerticalUpward){ + } else if (mTechnique != null && mTechnique == Technique.VerticalUpward) { - } else if (mTechnique != null && mTechnique == Technique.VerticalDownward){ + } 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 onAnimationStart(Animator animator) { + } + + @Override + public void onAnimationCancel(Animator animator) { + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + @Override public void onAnimationEnd(Animator animator) { view.setScaleX(1f); @@ -529,9 +557,18 @@ public void onAnimationEnd(Animator animator) { } }; 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 onAnimationStart(Animator animator) { + } + + @Override + public void onAnimationCancel(Animator animator) { + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + @Override public void onAnimationEnd(Animator animator) { view.setScaleX(1f); @@ -596,12 +633,35 @@ public void onAnimationEnd(Animator animator) { mFrameLayout.addAnimatorSet(animatorSet2); } } - private int getScreenWidth(){ - if (mActivity!=null) { + + private int getScreenWidth() { + if (mActivity != null) { return mActivity.getResources().getDisplayMetrics().widthPixels; } else { return 0; } } + private int getScreenHeight() { + if (mActivity != null) { + return mActivity.getResources().getDisplayMetrics().heightPixels; + } else { + return 0; + } + } + + /** + * 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 + } + } From 7d67ef287a4b86d1d96ef530043faced91bc2356 Mon Sep 17 00:00:00 2001 From: Dominik Mosberger Date: Wed, 13 Jul 2016 16:38:42 +0200 Subject: [PATCH 3/3] Added a second static pointer --- .../tourguide/FrameLayoutWithHole.java | 10 --- .../java/tourguide/tourguide/TourGuide.java | 62 +++++++++---------- 2 files changed, 29 insertions(+), 43 deletions(-) diff --git a/tourguide/src/main/java/tourguide/tourguide/FrameLayoutWithHole.java b/tourguide/src/main/java/tourguide/tourguide/FrameLayoutWithHole.java index b2d67fd..500c0f7 100644 --- a/tourguide/src/main/java/tourguide/tourguide/FrameLayoutWithHole.java +++ b/tourguide/src/main/java/tourguide/tourguide/FrameLayoutWithHole.java @@ -310,14 +310,4 @@ public int getScreenWidth(Activity activity){ public int getScreenHeight(Activity activity){ return activity.getResources().getDisplayMetrics().heightPixels; } - - public Rect getHighlightedView() { - int[] position = new int[2]; - mViewHole.getLocationOnScreen(position); - int left = position[0]; - int right = left + mViewHole.getWidth(); - int top = position[1]; - int bottom = top + mViewHole.getHeight(); - return new Rect(left, top, right, bottom); - } } diff --git a/tourguide/src/main/java/tourguide/tourguide/TourGuide.java b/tourguide/src/main/java/tourguide/tourguide/TourGuide.java index e1336fb..4fed1aa 100644 --- a/tourguide/src/main/java/tourguide/tourguide/TourGuide.java +++ b/tourguide/src/main/java/tourguide/tourguide/TourGuide.java @@ -5,6 +5,7 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.Activity; +import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.os.Build; @@ -18,6 +19,7 @@ import android.view.ViewTreeObserver; import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.TextView; /** @@ -33,8 +35,8 @@ public class TourGuide { protected FrameLayoutWithHole mFrameLayout; private Activity mActivity; private View mToolTipViewGroup; - private boolean mUseDefaultMeasurement; - private int mMaxBottom; + private View mCustomPointerView; + private boolean mCustomAlignmentForToolTip; /* Constructor */ public TourGuide(Activity activity) { @@ -50,10 +52,6 @@ public static TourGuide init(Activity activity) { return new TourGuide(activity); } - public int getMaxBottom() { - return mMaxBottom; - } - /** * Setter for the animation to be used * @@ -109,11 +107,6 @@ public void cleanUp() { } } - public TourGuide setUseDefaultMeasurement(boolean useDefaultMeasurement) { - mUseDefaultMeasurement = useDefaultMeasurement; - return this; - } - /** * @return FrameLayoutWithHole that is used as overlay */ @@ -150,6 +143,16 @@ public TourGuide setToolTip(ToolTip toolTip) { return this; } + public TourGuide setCustomPointerView(View customPointerView) { + mCustomPointerView = customPointerView; + return this; + } + + public TourGuide setCustomAlignmentForToolTip(Boolean customAlignmentForToolTip) { + mCustomAlignmentForToolTip = customAlignmentForToolTip; + return this; + } + /****** * Private methods *******/ @@ -281,18 +284,9 @@ private void setupToolTip() { mToolTipViewGroup.setBackgroundDrawable(mActivity.getResources().getDrawable(R.drawable.drop_shadow)); } - if (mUseDefaultMeasurement) { - Rect highlightedView = mFrameLayout.getHighlightedView(); - FrameLayout layout = new FrameLayout(mActivity); - FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(highlightedView.width(), getScreenHeight() - highlightedView.bottom); - layout.setLayoutParams(params); - layout.setX(highlightedView.left); - layout.setY(highlightedView.bottom); - layout.addView(mToolTipViewGroup); - - mMaxBottom = getScreenHeight() - highlightedView.bottom; + if (mCustomAlignmentForToolTip) { + parent.addView(mToolTipViewGroup); - parent.addView(layout, layoutParams); } else { /* position and size calculation */ int[] pos = new int[2]; @@ -369,9 +363,20 @@ public void onGlobalLayout() { // set the position using setMargins on the left and top layoutParams.setMargins(resultPoint.x, resultPoint.y, 0, 0); } + + if (mCustomPointerView != null) { + prepareCustomPointerView(parent, layoutParams); + } } } + private void prepareCustomPointerView(ViewGroup parent, FrameLayout.LayoutParams layoutParams) { + FrameLayout layout = new FrameLayout(mActivity); + layout.addView(mCustomPointerView, layoutParams); + + parent.addView(layout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } + private int getXForTooTip(int gravity, int toolTipMeasuredWidth, int targetViewX, float adjustment) { int x; if ((gravity & Gravity.LEFT) == Gravity.LEFT) { @@ -410,14 +415,13 @@ private FloatingActionButton setupAndAddFABToFrameLayout(final FrameLayoutWithHo fab.setSize(FloatingActionButton.SIZE_MINI); fab.setBackgroundColor(mPointer.mColor); fab.setClickable(false); - - int dimensionPixelSize = mActivity.getResources().getDimensionPixelSize(android.support.design.R.dimen.design_fab_size_mini); + fab.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); 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(dimensionPixelSize), getYBasedOnGravity(dimensionPixelSize), 0, 0); + params.setMargins(getXBasedOnGravity(fab.getMeasuredWidth()), getYBasedOnGravity(fab.getMeasuredHeight()), 0, 0); return fab; @@ -642,14 +646,6 @@ private int getScreenWidth() { } } - private int getScreenHeight() { - if (mActivity != null) { - return mActivity.getResources().getDisplayMetrics().heightPixels; - } else { - return 0; - } - } - /** * This describes the animation techniques */