add multiple steps to tutorialViews

This commit is contained in:
Phillip Thelen 2017-04-12 14:57:09 +02:00
parent e561e2db30
commit b9e07e4573
14 changed files with 272 additions and 61 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

View file

@ -1,26 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:clickable="true"
android:fitsSystemWindows="true"
android:id="@+id/background">
android:layout_height="match_parent"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:clickable="true"
android:fitsSystemWindows="true"
android:id="@+id/background">
<View android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@android:color/black"
android:alpha="0.6"/>
android:layout_width="match_parent"
android:background="@android:color/black"
android:alpha="0.6"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="24dp"
android:layout_alignParentBottom="true"
android:fitsSystemWindows="true"
android:layout_marginBottom="?actionBarSize">
<com.habitrpg.android.habitica.ui.SpeechBubbleView
android:id="@+id/speech_bubble"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:layout_alignParentBottom="true"
android:fitsSystemWindows="true"
android:layout_marginBottom="?actionBarSize">
<com.habitrpg.android.habitica.ui.SpeechBubbleView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:namePlate="Justin"
app:npcDrawable="@drawable/justin_textbox"/>
app:namePlate="Justin"
app:npcDrawable="@drawable/justin_textbox"/>
</FrameLayout>
</RelativeLayout>

View file

@ -19,16 +19,16 @@
android:layout_marginTop="46dp"
android:background="@drawable/white_rounded_bg"
android:orientation="vertical"
android:paddingBottom="14dp"
android:paddingBottom="16dp"
android:paddingLeft="21dp"
android:paddingRight="21dp"
android:paddingTop="16dp">
android:paddingTop="24dp"
android:layout_marginBottom="10dp">
<TextView
<com.habitrpg.android.habitica.ui.views.Typewriter
android:id="@+id/textView"
style="@style/Body1"
style="@style/Subheader1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="@string/welcome_text" />
@ -37,7 +37,8 @@
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
android:orientation="horizontal"
android:layout_marginTop="8dp">
<Button
android:id="@+id/dismissButton"
@ -61,6 +62,15 @@
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/continue_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/speechbubble_caret"
android:layout_gravity="bottom|center"
android:visibility="gone"
tools:visibility="visible"/>
<TextView
android:id="@+id/name_plate"
android:layout_width="wrap_content"

View file

@ -1,9 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tutorial_habits">Complete Positive Habits to earn gold and experience! Negative Habits will hurt your avatar if you tap them, so avoid them in real life!</string>
<string name="tutorial_dailies">Defeat your repeating Daily tasks to gain gold and experience. Danger! Dailies will hurt your avatar if you don\'t complete them in time.</string>
<string name="tutorial_todos">Complete your To-Dos in real life, then check them off for GOLD and EXPERIENCE so you can earn Rewards and unlock new features!</string>
<string name="tutorial_rewards">These are your Rewards! Earn gold by completing real-world Habits, Dailies, and To-Dos. Then spend it on in-game Rewards or custom real-world Rewards!</string>
<string name="tutorial_overview">Here we are! Ive filled out some tasks for you based on your interests. Try adding a few of your own. You can edit any task by tapping the title.</string>
<string name="tutorial_habits_1">First up is Habits. They can be positive Habits you want to improve or negative Habits you want to quit.</string>
<string name="tutorial_habits_2">Every time you do a positive Habit, tap the + to get experience and gold!</string>
<string name="tutorial_habits_3">If you slip up and do a negative Habit, tapping the - will reduce your avatars health to help you stay accountable.</string>
<string name="tutorial_habits_4">Give it a shot! You can explore the other task types through the bottom navigation.</string>
<string name="tutorial_dailies_1">Make Dailies for time sensitive tasks that need to be done on a regular schedule.</string>
<string name="tutorial_dailies_2">Be careful — if you miss one, your avatar will take damage overnight. Checking them off consistently brings great rewards!</string>
<string name="tutorial_todos_1">Use To-dos to keep track of tasks you need to do just once.</string>
<string name="tutorial_todos_2">If your To-do has to be done by a certain time, set a due date. Looks like you can check one off — go ahead!</string>
<string name="tutorial_rewards_1">Buy gear for your avatar with the gold you earn!</string>
<string name="tutorial_rewards_2">You can also make real-world Custom Rewards based on what motivates you.</string>
<string name="tutorial_equipment">When you buy equipment, it appears here. Your Battle Gear affects your stats, and your Costume (if enabled) affects what your avatar wears.</string>
<string name="tutorial_items">Earn items by completing tasks and leveling up. Tap on an item to use it!</string>

View file

@ -2,6 +2,8 @@ package com.habitrpg.android.habitica.events;
import com.magicmicky.habitrpgwrapper.lib.models.TutorialStep;
import java.util.List;
/**
* Created by viirus on 26/01/16.
*/
@ -10,4 +12,5 @@ public class DisplayTutorialEvent {
public TutorialStep step;
public String tutorialText;
public List<String> tutorialTexts;
}

View file

@ -2,6 +2,7 @@ package com.habitrpg.android.habitica.ui;
import com.habitrpg.android.habitica.R;
import com.habitrpg.android.habitica.ui.views.Typewriter;
import android.content.Context;
import android.content.res.TypedArray;
@ -9,6 +10,8 @@ import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@ -16,13 +19,13 @@ import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
public class SpeechBubbleView extends FrameLayout {
public class SpeechBubbleView extends FrameLayout implements View.OnClickListener {
@BindView(R.id.name_plate)
TextView namePlate;
@BindView(R.id.textView)
TextView textView;
Typewriter textView;
@BindView(R.id.npc_image_view)
ImageView npcImageView;
@ -30,6 +33,10 @@ public class SpeechBubbleView extends FrameLayout {
@BindView(R.id.confirmation_buttons)
ViewGroup confirmationButtons;
@BindView(R.id.continue_indicator)
View continueIndicator;
private ShowNextListener showNextListener;
public SpeechBubbleView(Context context, AttributeSet attrs) {
super(context, attrs);
@ -51,7 +58,43 @@ public class SpeechBubbleView extends FrameLayout {
}
confirmationButtons.setVisibility(View.GONE);
this.setOnClickListener(this);
}
public void setConfirmationButtonVisibility(int visibility) {
confirmationButtons.setVisibility(visibility);
}
public void animateText(String text) {
this.textView.animateText(text);
}
public void setText(String text) {
this.textView.setText(text);
}
public void setHasMoreContent(Boolean hasMoreContent) {
continueIndicator.setVisibility(hasMoreContent ? View.VISIBLE : View.GONE);
}
public void setShowNextListener(ShowNextListener listener) {
showNextListener = listener;
}
@Override
public void onClick(View v) {
if (textView.isAnimating()) {
textView.stopTextAnimation();
} else {
if (showNextListener != null) {
showNextListener.showNextStep();
}
}
}
public interface ShowNextListener {
void showNextStep();
}
}

View file

@ -1,63 +1,103 @@
package com.habitrpg.android.habitica.ui;
import android.content.Context;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import com.habitrpg.android.habitica.R;
import com.magicmicky.habitrpgwrapper.lib.models.TutorialStep;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.Objects;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class TutorialView extends FrameLayout implements View.OnClickListener {
public class TutorialView extends FrameLayout {
public TutorialStep step;
@Nullable
public OnTutorialReaction onReaction;
@BindView(R.id.textView)
TextView tutorialTextView;
@BindView(R.id.speech_bubble)
SpeechBubbleView speechBubbleView;
@BindView(R.id.background)
RelativeLayout background;
@BindView(R.id.dismissButton)
Button dismissButton;
@BindView(R.id.completeButton)
Button completeButton;
@BindView(R.id.confirmation_buttons)
ViewGroup confirmationButtons;
public TutorialView(Context context, TutorialStep step, OnTutorialReaction onReaction) {
private List<String> tutorialTexts;
private int currentTextIndex;
public TutorialView(Context context, TutorialStep step, @Nullable OnTutorialReaction onReaction) {
super(context);
LayoutInflater mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mInflater.inflate(R.layout.overlay_tutorial, this, true);
ButterKnife.bind(this);
background.setOnClickListener(this);
dismissButton.setOnClickListener(this);
completeButton.setOnClickListener(this);
confirmationButtons.setVisibility(View.VISIBLE);
speechBubbleView.setConfirmationButtonVisibility(View.GONE);
speechBubbleView.setShowNextListener(this::displayNextTutorialText);
this.step = step;
this.onReaction = onReaction;
}
public void setTutorialText(String text) {
this.tutorialTextView.setText(text);
this.speechBubbleView.animateText(text);
speechBubbleView.setConfirmationButtonVisibility(View.VISIBLE);
}
@Override
public void onClick(View v) {
if (Objects.equals(v, background) || Objects.equals(v, completeButton)) {
public void setTutorialTexts(List<String> texts) {
tutorialTexts = texts;
currentTextIndex = -1;
displayNextTutorialText();
}
private void displayNextTutorialText() {
currentTextIndex++;
if (currentTextIndex < tutorialTexts.size()) {
speechBubbleView.animateText(tutorialTexts.get(currentTextIndex));
if (isDisplayingLastStep()) {
speechBubbleView.setConfirmationButtonVisibility(View.VISIBLE);
speechBubbleView.setHasMoreContent(false);
} else {
speechBubbleView.setHasMoreContent(true);
}
}
}
@OnClick(R.id.completeButton)
public void completeButtonClicked() {
if (this.onReaction != null) {
this.onReaction.onTutorialCompleted(this.step);
} else if (Objects.equals(v, dismissButton)) {
}
}
@OnClick(R.id.dismissButton)
public void dismissButtonClicked() {
if (this.onReaction != null) {
this.onReaction.onTutorialDeferred(this.step);
}
}
@OnClick(R.id.background)
public void backgroundClicked() {
if (isDisplayingLastStep()) {
if (this.onReaction != null) {
this.onReaction.onTutorialCompleted(this.step);
}
} else {
speechBubbleView.onClick(speechBubbleView);
}
}
private boolean isDisplayingLastStep() {
return currentTextIndex == (tutorialTexts.size()-1);
}
public interface OnTutorialReaction {
void onTutorialCompleted(TutorialStep step);

View file

@ -869,7 +869,11 @@ public class MainActivity extends BaseActivity implements Action1<Throwable>, Ha
@Subscribe
public void onEvent(DisplayTutorialEvent tutorialEvent) {
this.displayTutorialStep(tutorialEvent.step, tutorialEvent.tutorialText);
if (tutorialEvent.tutorialText != null) {
this.displayTutorialStep(tutorialEvent.step, tutorialEvent.tutorialText);
} else {
this.displayTutorialStep(tutorialEvent.step, tutorialEvent.tutorialTexts);
}
}
@Subscribe
@ -1091,6 +1095,20 @@ public class MainActivity extends BaseActivity implements Action1<Throwable>, Ha
AmplitudeManager.sendEvent("tutorial", AmplitudeManager.EVENT_CATEGORY_BEHAVIOUR, AmplitudeManager.EVENT_HITTYPE_EVENT, additionalData);
}
private void displayTutorialStep(TutorialStep step, List<String> texts) {
TutorialView view = new TutorialView(this, step, this);
view.setTutorialTexts(texts);
view.onReaction = this;
this.overlayLayout.addView(view);
this.activeTutorialView = view;
Map<String, Object> additionalData = new HashMap<>();
additionalData.put("eventLabel", step.getIdentifier() + "-android");
additionalData.put("eventValue", step.getIdentifier());
additionalData.put("complete", false);
AmplitudeManager.sendEvent("tutorial", AmplitudeManager.EVENT_CATEGORY_BEHAVIOUR, AmplitudeManager.EVENT_HITTYPE_EVENT, additionalData);
}
@Override
public void onTutorialCompleted(TutorialStep step) {
String path = "flags.tutorial." + step.getTutorialGroup() + "." + step.getIdentifier();

View file

@ -25,6 +25,7 @@ import org.greenrobot.eventbus.EventBusException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import butterknife.ButterKnife;
@ -41,7 +42,11 @@ public abstract class BaseFragment extends DialogFragment {
if (step != null && !step.getWasCompleted() && (step.getDisplayedOn() == null || (new Date().getTime() - step.getDisplayedOn().getTime()) > 86400000)) {
DisplayTutorialEvent event = new DisplayTutorialEvent();
event.step = step;
event.tutorialText = tutorialText;
if (tutorialText != null) {
event.tutorialText = tutorialText;
} else {
event.tutorialTexts = tutorialTexts;
}
EventBus.getDefault().post(event);
}
}
@ -56,6 +61,7 @@ public abstract class BaseFragment extends DialogFragment {
return true;
}
};
public List<String> tutorialTexts;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {

View file

@ -43,6 +43,8 @@ import com.magicmicky.habitrpgwrapper.lib.models.tasks.Task;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import java.util.Arrays;
import javax.inject.Inject;
import javax.inject.Named;
@ -100,22 +102,29 @@ public class TaskRecyclerViewFragment extends BaseFragment implements View.OnCli
switch (fragment.classType) {
case Task.TYPE_HABIT: {
fragment.tutorialStepIdentifier = "habits";
fragment.tutorialText = context.getString(R.string.tutorial_habits);
fragment.tutorialTexts = Arrays.asList(context.getString(R.string.tutorial_overview),
context.getString(R.string.tutorial_habits_1),
context.getString(R.string.tutorial_habits_2),
context.getString(R.string.tutorial_habits_3),
context.getString(R.string.tutorial_habits_4));
break;
}
case Task.FREQUENCY_DAILY: {
fragment.tutorialStepIdentifier = "dailies";
fragment.tutorialText = context.getString(R.string.tutorial_dailies);
fragment.tutorialTexts = Arrays.asList(context.getString(R.string.tutorial_dailies_1),
context.getString(R.string.tutorial_dailies_2));
break;
}
case Task.TYPE_TODO: {
fragment.tutorialStepIdentifier = "todos";
fragment.tutorialText = context.getString(R.string.tutorial_todos);
fragment.tutorialTexts = Arrays.asList(context.getString(R.string.tutorial_todos_1),
context.getString(R.string.tutorial_todos_2));
break;
}
case Task.TYPE_REWARD: {
fragment.tutorialStepIdentifier = "rewards";
fragment.tutorialText = context.getString(R.string.tutorial_rewards);
fragment.tutorialTexts = Arrays.asList(context.getString(R.string.tutorial_rewards_1),
context.getString(R.string.tutorial_rewards_2));
break;
}
}

View file

@ -0,0 +1,72 @@
package com.habitrpg.android.habitica.ui.views;
import android.content.Context;
import android.os.Handler;
import android.support.v4.content.ContextCompat;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.util.AttributeSet;
import com.habitrpg.android.habitica.R;
// http://stackoverflow.com/a/6700718/1315039
public class Typewriter extends android.support.v7.widget.AppCompatTextView {
private SpannableStringBuilder stringBuilder;
private Object visibleSpan;
private Object hiddenSpan;
private int index;
private long delay = 30;
public Typewriter(Context context) {
super(context);
setupTextColors(context);
}
public Typewriter(Context context, AttributeSet attrs) {
super(context, attrs);
setupTextColors(context);
}
private void setupTextColors(Context context) {
visibleSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.textColorLight));
hiddenSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.transparent));
}
private Handler handler = new Handler();
private Runnable characterAdder = new Runnable() {
@Override
public void run() {
stringBuilder.setSpan(visibleSpan, 0, index++, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
setText(stringBuilder);
if(index <= stringBuilder.length()) {
handler.postDelayed(characterAdder, delay);
}
}
};
public void animateText(CharSequence text) {
stringBuilder = new SpannableStringBuilder(text);
stringBuilder.setSpan(hiddenSpan, 0, stringBuilder.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
index = 0;
setText(stringBuilder);
handler.removeCallbacks(characterAdder);
handler.postDelayed(characterAdder, delay);
}
public void setCharacterDelay(long millis) {
delay = millis;
}
public boolean isAnimating() {
return index < stringBuilder.length();
}
public void stopTextAnimation() {
index = stringBuilder.length();
}
}