diff --git a/.gitignore b/.gitignore index a287a2a..afbdab3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,57 +1,6 @@ +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries .DS_Store - -# Local sandbox -_sandbox - -# built application files -*.apk -*.ap_ - -# files for the dex VM -*.dex - -# Java class files -*.class - -# generated files -out/ -bin/ -gen/ - -# Local configuration file (sdk path, etc) -local.properties - -# Eclipse project files -.classpath -.project - -# IDEA files -*.iml -*.ipr -*.iws - -# Other build files -project.properties -ant.properties -build.xml -proguard-project.txt - -# Built application files -/*/build/ /build - -# Gradle generated files -.gradle/ - -# User-specific configurations -.idea/libraries/ -.idea/workspace.xml -.idea/tasks.xml -.idea/.name -.idea/compiler.xml -.idea/copyright/profiles_settings.xml -.idea/encodings.xml -.idea/misc.xml -.idea/modules.xml -.idea/scopes/scope_settings.xml -.idea/vcs.xml \ No newline at end of file diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..e2fb291 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Android-Material-Wizard \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..217af47 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..e206d70 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 2caec4f..aaafd26 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -3,15 +3,17 @@ - - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3f9d109 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + Abstraction issues + + + + + + + + + + + + + 1.7 + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..4ad9c50 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..c80f219 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/README.md b/README.md index 1f99ad1..18d3cd4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ -Android WizardPager Sample Code +Android Lollipop wizard template =============================== -Example `ViewPager`-based wizard UI sample code. See [my Google+ post](https://plus.google.com/+RomanNurik/posts/6cVymZvn3f4) for more details. +Fork of Roman Nurik's Example `ViewPager`-based wizard UI sample code. For further information on his wizard, see [his Google+ post](https://plus.google.com/+RomanNurik/posts/6cVymZvn3f4) or [his Github project page](https://github.com/romannurik/android-wizardpager). - - - +This template wizard is a Lollipop version of the original wizard. -Additional page type examples (boolean and single text field) can be found in str4d's fork (`model`, `ui`). + + + + + +I've provided a GIF showing the wizard in action. + +![Material Wizard GIF](http://i.imgur.com/PImgBs0.gif) diff --git a/Wizards.iml b/Wizards.iml new file mode 100644 index 0000000..0bb6048 --- /dev/null +++ b/Wizards.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 0000000..74ba43b --- /dev/null +++ b/app/app.iml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..d83b9f5 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 22 + buildToolsVersion "21.1.2" + + defaultConfig { + applicationId "com.markosullivan.wizards" + minSdkVersion 21 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile "com.android.support:support-v4:22.0.+" +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..a64ee44 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\Users\MOS182\Documents\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/java/com/markosullivan/wizards/ApplicationTest.java b/app/src/androidTest/java/com/markosullivan/wizards/ApplicationTest.java new file mode 100644 index 0000000..ff64db9 --- /dev/null +++ b/app/src/androidTest/java/com/markosullivan/wizards/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.markosullivan.wizards; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d6b4342 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/markosullivan/wizards/MainActivity.java b/app/src/main/java/com/markosullivan/wizards/MainActivity.java new file mode 100644 index 0000000..6b0b19c --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/MainActivity.java @@ -0,0 +1,261 @@ +package com.markosullivan.wizards; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.ViewPager; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import com.markosullivan.wizards.wizard.model.AbstractWizardModel; +import com.markosullivan.wizards.wizard.model.ModelCallbacks; +import com.markosullivan.wizards.wizard.model.Page; +import com.markosullivan.wizards.wizard.ui.PageFragmentCallbacks; +import com.markosullivan.wizards.wizard.ui.ReviewFragment; +import com.markosullivan.wizards.wizard.ui.StepPagerStrip; + +import java.util.List; + +public class MainActivity extends FragmentActivity implements + PageFragmentCallbacks, + ReviewFragment.Callbacks, + ModelCallbacks { + private ViewPager mPager; + private MyPagerAdapter mPagerAdapter; + + private boolean mEditingAfterReview; + + private AbstractWizardModel mWizardModel = new PresentWizardModel(this); + + private boolean mConsumePageSelectedEvent; + + private Button mNextButton; + private Button mPrevButton; + + private List mCurrentPageSequence; + private StepPagerStrip mStepPagerStrip; + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + if (savedInstanceState != null) { + mWizardModel.load(savedInstanceState.getBundle("model")); + } + + mWizardModel.registerListener(this); + + mPagerAdapter = new MyPagerAdapter(getSupportFragmentManager()); + mPager = (ViewPager) findViewById(R.id.pager); + mPager.setAdapter(mPagerAdapter); + mStepPagerStrip = (StepPagerStrip) findViewById(R.id.strip); + mStepPagerStrip.setOnPageSelectedListener(new StepPagerStrip.OnPageSelectedListener() { + @Override + public void onPageStripSelected(int position) { + position = Math.min(mPagerAdapter.getCount() - 1, position); + if (mPager.getCurrentItem() != position) { + mPager.setCurrentItem(position); + } + } + }); + + mNextButton = (Button) findViewById(R.id.next_button); + mPrevButton = (Button) findViewById(R.id.prev_button); + + mPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + mStepPagerStrip.setCurrentPage(position); + + if (mConsumePageSelectedEvent) { + mConsumePageSelectedEvent = false; + return; + } + + mEditingAfterReview = false; + updateBottomBar(); + } + }); + + mNextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mPager.getCurrentItem() == mCurrentPageSequence.size()) { + DialogFragment dg = new DialogFragment() { + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new AlertDialog.Builder(getActivity()) + .setMessage(R.string.submit_confirm_message) + .setPositiveButton(R.string.submit_confirm_button, null) + .setNegativeButton(android.R.string.cancel, null) + .create(); + } + }; + dg.show(getSupportFragmentManager(), "place_order_dialog"); + } else { + if (mEditingAfterReview) { + mPager.setCurrentItem(mPagerAdapter.getCount() - 1); + } else { + mPager.setCurrentItem(mPager.getCurrentItem() + 1); + } + } + } + }); + + mPrevButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mPager.setCurrentItem(mPager.getCurrentItem() - 1); + } + }); + + onPageTreeChanged(); + updateBottomBar(); + } + + @Override + public void onPageTreeChanged() { + mCurrentPageSequence = mWizardModel.getCurrentPageSequence(); + recalculateCutOffPage(); + mStepPagerStrip.setPageCount(mCurrentPageSequence.size() + 1); // + 1 = review step + mPagerAdapter.notifyDataSetChanged(); + updateBottomBar(); + } + + private void updateBottomBar() { + int position = mPager.getCurrentItem(); + if (position == mCurrentPageSequence.size()) { + mNextButton.setText(R.string.finish); + } else { + mNextButton.setText(mEditingAfterReview + ? R.string.review + : R.string.next); + } + + mPrevButton.setVisibility(position <= 0 ? View.INVISIBLE : View.VISIBLE); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mWizardModel.unregisterListener(this); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBundle("model", mWizardModel.save()); + } + + @Override + public AbstractWizardModel onGetModel() { + return mWizardModel; + } + + @Override + public void onEditScreenAfterReview(String key) { + for (int i = mCurrentPageSequence.size() - 1; i >= 0; i--) { + if (mCurrentPageSequence.get(i).getKey().equals(key)) { + mConsumePageSelectedEvent = true; + mEditingAfterReview = true; + mPager.setCurrentItem(i); + updateBottomBar(); + break; + } + } + } + + @Override + public void onPageDataChanged(Page page) { + if (page.isRequired()) { + if (recalculateCutOffPage()) { + mPagerAdapter.notifyDataSetChanged(); + updateBottomBar(); + } + } + } + + @Override + public Page onGetPage(String key) { + return mWizardModel.findByKey(key); + } + + private boolean recalculateCutOffPage() { + // Cut off the pager adapter at first required page that isn't completed + int cutOffPage = mCurrentPageSequence.size() + 1; + for (int i = 0; i < mCurrentPageSequence.size(); i++) { + Page page = mCurrentPageSequence.get(i); + if (page.isRequired() && !page.isCompleted()) { + cutOffPage = i; + break; + } + } + + if (mPagerAdapter.getCutOffPage() != cutOffPage) { + mPagerAdapter.setCutOffPage(cutOffPage); + return true; + } + + return false; + } + + public class MyPagerAdapter extends FragmentStatePagerAdapter { + private int mCutOffPage; + private Fragment mPrimaryItem; + + public MyPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int i) { + if (i >= mCurrentPageSequence.size()) { + return new ReviewFragment(); + } + + return mCurrentPageSequence.get(i).createFragment(); + } + + @Override + public int getItemPosition(Object object) { + // TODO: be smarter about this + if (object == mPrimaryItem) { + // Re-use the current fragment (its position never changes) + return POSITION_UNCHANGED; + } + + return POSITION_NONE; + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + super.setPrimaryItem(container, position, object); + mPrimaryItem = (Fragment) object; + } + + @Override + public int getCount() { + if (mCurrentPageSequence == null) { + return 0; + } + return Math.min(mCutOffPage + 1, mCurrentPageSequence.size() + 1); + } + + public void setCutOffPage(int cutOffPage) { + if (cutOffPage < 0) { + cutOffPage = Integer.MAX_VALUE; + } + mCutOffPage = cutOffPage; + } + + public int getCutOffPage() { + return mCutOffPage; + } + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/PresentWizardModel.java b/app/src/main/java/com/markosullivan/wizards/PresentWizardModel.java new file mode 100644 index 0000000..7055799 --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/PresentWizardModel.java @@ -0,0 +1,75 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards; + +import android.content.Context; + +import com.markosullivan.wizards.wizard.model.AbstractWizardModel; +import com.markosullivan.wizards.wizard.model.BranchPage; +import com.markosullivan.wizards.wizard.model.InstructionPage; +import com.markosullivan.wizards.wizard.model.MultipleFixedChoicePage; +import com.markosullivan.wizards.wizard.model.PageList; +import com.markosullivan.wizards.wizard.model.SingleFixedChoicePage; + +public class PresentWizardModel extends AbstractWizardModel { + public PresentWizardModel(Context context) { + super(context); + } + + @Override + protected PageList onNewRootPageList() { + return new PageList( + + // BranchPage shows all of the branches available: Branch One, Branch Two, Branch Three. Each of these branches + // have their own questions and the choices of the user will be summarised in the review section at the end + new BranchPage(this, "Select one options") + .addBranch("Branch One", + new SingleFixedChoicePage(this, "Question One") + .setChoices("A", "B", "C", "D") + .setRequired(true), + + new MultipleFixedChoicePage(this, "Question Two") + .setChoices("A", "B", "C", "D", + "E") + ) + + // Second branch of questions + .addBranch("Branch Two", + new SingleFixedChoicePage(this, "Question One") + .setChoices("A", "B") + .setRequired(true), + + new SingleFixedChoicePage(this, "Question Two") + .setChoices("A", "B", "C", + "D", "E", "F") + .setRequired(true), + + new SingleFixedChoicePage(this, "Question Three") + .setChoices("A", "B", "C") + ) + + // Third branch of questions + .addBranch("Branch Three", + new InstructionPage(this, "Info"), + + new SingleFixedChoicePage(this, "Question One") + .setChoices("A", "B", "C") + .setRequired(true) + ) + ); + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/model/AbstractWizardModel.java b/app/src/main/java/com/markosullivan/wizards/wizard/model/AbstractWizardModel.java new file mode 100644 index 0000000..7e1725f --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/model/AbstractWizardModel.java @@ -0,0 +1,100 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.model; + +import android.content.Context; +import android.os.Bundle; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a wizard model, including the pages/steps in the wizard, their dependencies, and their + * currently populated choices/values/selections. + * + * To create an actual wizard model, extend this class and implement {@link #onNewRootPageList()}. + */ +public abstract class AbstractWizardModel implements ModelCallbacks { + protected Context mContext; + + private List mListeners = new ArrayList(); + private PageList mRootPageList; + + public AbstractWizardModel(Context context) { + mContext = context; + mRootPageList = onNewRootPageList(); + } + + /** + * Override this to define a new wizard model. + */ + protected abstract PageList onNewRootPageList(); + + @Override + public void onPageDataChanged(Page page) { + // can't use for each because of concurrent modification (review fragment + // can get added or removed and will register itself as a listener) + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).onPageDataChanged(page); + } + } + + @Override + public void onPageTreeChanged() { + // can't use for each because of concurrent modification (review fragment + // can get added or removed and will register itself as a listener) + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).onPageTreeChanged(); + } + } + + public Page findByKey(String key) { + return mRootPageList.findByKey(key); + } + + public void load(Bundle savedValues) { + for (String key : savedValues.keySet()) { + mRootPageList.findByKey(key).resetData(savedValues.getBundle(key)); + } + } + + public void registerListener(ModelCallbacks listener) { + mListeners.add(listener); + } + + public Bundle save() { + Bundle bundle = new Bundle(); + for (Page page : getCurrentPageSequence()) { + bundle.putBundle(page.getKey(), page.getData()); + } + return bundle; + } + + /** + * Gets the current list of wizard steps, flattening nested (dependent) pages based on the + * user's choices. + */ + public List getCurrentPageSequence() { + ArrayList flattened = new ArrayList(); + mRootPageList.flattenCurrentPageSequence(flattened); + return flattened; + } + + public void unregisterListener(ModelCallbacks listener) { + mListeners.remove(listener); + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/model/BranchPage.java b/app/src/main/java/com/markosullivan/wizards/wizard/model/BranchPage.java new file mode 100644 index 0000000..b1f3427 --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/model/BranchPage.java @@ -0,0 +1,122 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.model; + +import android.support.v4.app.Fragment; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +import com.markosullivan.wizards.wizard.ui.SingleChoiceFragment; + +/** + * A page representing a branching point in the wizard. Depending on which choice is selected, the + * next set of steps in the wizard may change. + */ +public class BranchPage extends SingleFixedChoicePage { + private List mBranches = new ArrayList(); + + public BranchPage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + @Override + public Page findByKey(String key) { + if (getKey().equals(key)) { + return this; + } + + for (Branch branch : mBranches) { + Page found = branch.childPageList.findByKey(key); + if (found != null) { + return found; + } + } + + return null; + } + + @Override + public void flattenCurrentPageSequence(ArrayList destination) { + super.flattenCurrentPageSequence(destination); + for (Branch branch : mBranches) { + if (branch.choice.equals(mData.getString(Page.SIMPLE_DATA_KEY))) { + branch.childPageList.flattenCurrentPageSequence(destination); + break; + } + } + } + + public BranchPage addBranch(String choice, Page... childPages) { + PageList childPageList = new PageList(childPages); + for (Page page : childPageList) { + page.setParentKey(choice); + } + mBranches.add(new Branch(choice, childPageList)); + return this; + } + + public BranchPage addBranch(String choice) { + mBranches.add(new Branch(choice, new PageList())); + return this; + } + + @Override + public Fragment createFragment() { + return SingleChoiceFragment.create(getKey()); + } + + public String getOptionAt(int position) { + return mBranches.get(position).choice; + } + + public int getOptionCount() { + return mBranches.size(); + } + + @Override + public void getReviewItems(ArrayList dest) { + dest.add(new ReviewItem(getTitle(), mData.getString(SIMPLE_DATA_KEY), getKey())); + } + + @Override + public boolean isCompleted() { + return !TextUtils.isEmpty(mData.getString(SIMPLE_DATA_KEY)); + } + + @Override + public void notifyDataChanged() { + mCallbacks.onPageTreeChanged(); + super.notifyDataChanged(); + } + + public BranchPage setValue(String value) { + mData.putString(SIMPLE_DATA_KEY, value); + return this; + } + + private static class Branch { + public String choice; + public PageList childPageList; + + private Branch(String choice, PageList childPageList) { + this.choice = choice; + this.childPageList = childPageList; + } + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/model/CustomerInfoPage.java b/app/src/main/java/com/markosullivan/wizards/wizard/model/CustomerInfoPage.java new file mode 100644 index 0000000..aa3c8af --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/model/CustomerInfoPage.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.model; + +import android.support.v4.app.Fragment; +import android.text.TextUtils; + +import com.markosullivan.wizards.wizard.ui.CustomerInfoFragment; + +import java.util.ArrayList; + +/** + * A page asking for a name and an email. + */ +public class CustomerInfoPage extends Page { + public static final String NAME_DATA_KEY = "name"; + public static final String EMAIL_DATA_KEY = "email"; + + public CustomerInfoPage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + @Override + public Fragment createFragment() { + return CustomerInfoFragment.create(getKey()); + } + + @Override + public void getReviewItems(ArrayList dest) { + dest.add(new ReviewItem("Your name", mData.getString(NAME_DATA_KEY), getKey(), -1)); + dest.add(new ReviewItem("Your email", mData.getString(EMAIL_DATA_KEY), getKey(), -1)); + } + + @Override + public boolean isCompleted() { + return !TextUtils.isEmpty(mData.getString(NAME_DATA_KEY)); + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/model/InstructionPage.java b/app/src/main/java/com/markosullivan/wizards/wizard/model/InstructionPage.java new file mode 100644 index 0000000..03fba67 --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/model/InstructionPage.java @@ -0,0 +1,73 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.model; + +import android.support.v4.app.Fragment; +import android.text.TextUtils; + +import com.markosullivan.wizards.wizard.ui.InstructionFragment; + +import java.util.ArrayList; + +/** + * A page offering the user a number of mutually exclusive choices. + */ +public class InstructionPage extends Page { + protected ArrayList mChoices = new ArrayList(); + + public InstructionPage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + @Override + public Fragment createFragment() { + return InstructionFragment.create(getKey()); + } + + public String getOptionAt(int position) { + return mChoices.get(position); + } + + public int getOptionCount() { + return mChoices.size(); + } + + @Override + public void getReviewItems(ArrayList dest) { + + /* + The line below is commented out to prevent another ReviewItem being added to the review + at the end of the wizard. If you want to enable this the value displayed will be '(None)' + but you can change this value by changing the value inside mData.getString() + */ + + + //dest.add(new ReviewItem(getTitle(), mData.getString(SIMPLE_DATA_KEY), getKey())); + } + + @Override + public boolean isCompleted() { + return !TextUtils.isEmpty(mData.getString(SIMPLE_DATA_KEY)); + } + + public InstructionPage setValue(String value) { + mData.putString(SIMPLE_DATA_KEY, value); + return this; + } + + +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/model/ModelCallbacks.java b/app/src/main/java/com/markosullivan/wizards/wizard/model/ModelCallbacks.java new file mode 100644 index 0000000..6908976 --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/model/ModelCallbacks.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.model; + +/** + * Callback interface connecting {@link Page}, {@link AbstractWizardModel}, and model container + * objects (e.g. {@link mos.present.MainActivity}. + */ +public interface ModelCallbacks { + void onPageDataChanged(Page page); + void onPageTreeChanged(); +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/model/MultipleFixedChoicePage.java b/app/src/main/java/com/markosullivan/wizards/wizard/model/MultipleFixedChoicePage.java new file mode 100644 index 0000000..000ccef --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/model/MultipleFixedChoicePage.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.model; + +import android.support.v4.app.Fragment; + +import com.markosullivan.wizards.wizard.ui.MultipleChoiceFragment; + +import java.util.ArrayList; + +/** + * A page offering the user a number of non-mutually exclusive choices. + */ +public class MultipleFixedChoicePage extends SingleFixedChoicePage { + public MultipleFixedChoicePage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + @Override + public Fragment createFragment() { + return MultipleChoiceFragment.create(getKey()); + } + + @Override + public void getReviewItems(ArrayList dest) { + StringBuilder sb = new StringBuilder(); + + ArrayList selections = mData.getStringArrayList(Page.SIMPLE_DATA_KEY); + if (selections != null && selections.size() > 0) { + for (String selection : selections) { + if (sb.length() > 0) { + sb.append(", "); + } + sb.append(selection); + } + } + + dest.add(new ReviewItem(getTitle(), sb.toString(), getKey())); + } + + @Override + public boolean isCompleted() { + ArrayList selections = mData.getStringArrayList(Page.SIMPLE_DATA_KEY); + return selections != null && selections.size() > 0; + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/model/Page.java b/app/src/main/java/com/markosullivan/wizards/wizard/model/Page.java new file mode 100644 index 0000000..266c68f --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/model/Page.java @@ -0,0 +1,99 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.model; + +import android.os.Bundle; +import android.support.v4.app.Fragment; + +import java.util.ArrayList; + +/** + * Represents a single page in the wizard. + */ +public abstract class Page implements PageTreeNode { + /** + * The key into {@link #getData()} used for wizards with simple (single) values. + */ + public static final String SIMPLE_DATA_KEY = "_"; + + protected ModelCallbacks mCallbacks; + + /** + * Current wizard values/selections. + */ + protected Bundle mData = new Bundle(); + protected String mTitle; + protected boolean mRequired = false; + protected String mParentKey; + + protected Page(ModelCallbacks callbacks, String title) { + mCallbacks = callbacks; + mTitle = title; + } + + public Bundle getData() { + return mData; + } + + public String getTitle() { + return mTitle; + } + + public boolean isRequired() { + return mRequired; + } + + void setParentKey(String parentKey) { + mParentKey = parentKey; + } + + @Override + public Page findByKey(String key) { + return getKey().equals(key) ? this : null; + } + + @Override + public void flattenCurrentPageSequence(ArrayList dest) { + dest.add(this); + } + + public abstract Fragment createFragment(); + + public String getKey() { + return (mParentKey != null) ? mParentKey + ":" + mTitle : mTitle; + } + + public abstract void getReviewItems(ArrayList dest); + + public boolean isCompleted() { + return true; + } + + public void resetData(Bundle data) { + mData = data; + notifyDataChanged(); + } + + public void notifyDataChanged() { + mCallbacks.onPageDataChanged(this); + } + + public Page setRequired(boolean required) { + mRequired = required; + return this; + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/model/PageList.java b/app/src/main/java/com/markosullivan/wizards/wizard/model/PageList.java new file mode 100644 index 0000000..b0766c0 --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/model/PageList.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.model; + +import java.util.ArrayList; + +/** + * Represents a list of wizard pages. + */ +public class PageList extends ArrayList implements PageTreeNode { + + public PageList() { + + } + + public PageList(Page... pages) { + for (Page page : pages) { + add(page); + } + } + + @Override + public Page findByKey(String key) { + for (Page childPage : this) { + Page found = childPage.findByKey(key); + if (found != null) { + return found; + } + } + + return null; + } + + @Override + public void flattenCurrentPageSequence(ArrayList dest) { + for (Page childPage : this) { + childPage.flattenCurrentPageSequence(dest); + } + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/model/PageTreeNode.java b/app/src/main/java/com/markosullivan/wizards/wizard/model/PageTreeNode.java new file mode 100644 index 0000000..fd512da --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/model/PageTreeNode.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.model; + +import java.util.ArrayList; + +/** + * Represents a node in the page tree. Can either be a single page, or a page container. + */ +public interface PageTreeNode { + public Page findByKey(String key); + public void flattenCurrentPageSequence(ArrayList dest); +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/model/ReviewItem.java b/app/src/main/java/com/markosullivan/wizards/wizard/model/ReviewItem.java new file mode 100644 index 0000000..cb63657 --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/model/ReviewItem.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.model; + +/** + * Represents a single line item on the final review page. + * + * @see com.markosullivan.wizards.wizard.ui.ReviewFragment + */ +public class ReviewItem { + public static final int DEFAULT_WEIGHT = 0; + + private int mWeight; + private String mTitle; + private String mDisplayValue; + private String mPageKey; + + public ReviewItem(String title, String displayValue, String pageKey) { + this(title, displayValue, pageKey, DEFAULT_WEIGHT); + } + + public ReviewItem(String title, String displayValue, String pageKey, int weight) { + mTitle = title; + mDisplayValue = displayValue; + mPageKey = pageKey; + mWeight = weight; + } + + public String getDisplayValue() { + return mDisplayValue; + } + + public void setDisplayValue(String displayValue) { + mDisplayValue = displayValue; + } + + public String getPageKey() { + return mPageKey; + } + + public void setPageKey(String pageKey) { + mPageKey = pageKey; + } + + public String getTitle() { + return mTitle; + } + + public void setTitle(String title) { + mTitle = title; + } + + public int getWeight() { + return mWeight; + } + + public void setWeight(int weight) { + mWeight = weight; + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/model/SingleFixedChoicePage.java b/app/src/main/java/com/markosullivan/wizards/wizard/model/SingleFixedChoicePage.java new file mode 100644 index 0000000..4fb42f5 --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/model/SingleFixedChoicePage.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.model; + +import android.support.v4.app.Fragment; +import android.text.TextUtils; + +import com.markosullivan.wizards.wizard.ui.SingleChoiceFragment; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * A page offering the user a number of mutually exclusive choices. + */ +public class SingleFixedChoicePage extends Page { + protected ArrayList mChoices = new ArrayList(); + + public SingleFixedChoicePage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + @Override + public Fragment createFragment() { + return SingleChoiceFragment.create(getKey()); + } + + public String getOptionAt(int position) { + return mChoices.get(position); + } + + public int getOptionCount() { + return mChoices.size(); + } + + @Override + public void getReviewItems(ArrayList dest) { + dest.add(new ReviewItem(getTitle(), mData.getString(SIMPLE_DATA_KEY), getKey())); + } + + @Override + public boolean isCompleted() { + return !TextUtils.isEmpty(mData.getString(SIMPLE_DATA_KEY)); + } + + public SingleFixedChoicePage setChoices(String... choices) { + mChoices.addAll(Arrays.asList(choices)); + return this; + } + + public SingleFixedChoicePage setValue(String value) { + mData.putString(SIMPLE_DATA_KEY, value); + return this; + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/ui/CustomerInfoFragment.java b/app/src/main/java/com/markosullivan/wizards/wizard/ui/CustomerInfoFragment.java new file mode 100644 index 0000000..32558ef --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/ui/CustomerInfoFragment.java @@ -0,0 +1,150 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.ui; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; + +import com.markosullivan.wizards.R; +import com.markosullivan.wizards.wizard.model.CustomerInfoPage; + +public class CustomerInfoFragment extends Fragment { + private static final String ARG_KEY = "key"; + + private PageFragmentCallbacks mCallbacks; + private String mKey; + private CustomerInfoPage mPage; + private TextView mNameView; + private TextView mEmailView; + + public static CustomerInfoFragment create(String key) { + Bundle args = new Bundle(); + args.putString(ARG_KEY, key); + + CustomerInfoFragment fragment = new CustomerInfoFragment(); + fragment.setArguments(args); + return fragment; + } + + public CustomerInfoFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + mKey = args.getString(ARG_KEY); + mPage = (CustomerInfoPage) mCallbacks.onGetPage(mKey); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_page_customer_info, container, false); + ((TextView) rootView.findViewById(android.R.id.title)).setText(mPage.getTitle()); + + mNameView = ((TextView) rootView.findViewById(R.id.your_name)); + mNameView.setText(mPage.getData().getString(CustomerInfoPage.NAME_DATA_KEY)); + + mEmailView = ((TextView) rootView.findViewById(R.id.your_email)); + mEmailView.setText(mPage.getData().getString(CustomerInfoPage.EMAIL_DATA_KEY)); + return rootView; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (!(activity instanceof PageFragmentCallbacks)) { + throw new ClassCastException("Activity must implement PageFragmentCallbacks"); + } + + mCallbacks = (PageFragmentCallbacks) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mNameView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, + int i2) { + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + } + + @Override + public void afterTextChanged(Editable editable) { + mPage.getData().putString(CustomerInfoPage.NAME_DATA_KEY, + (editable != null) ? editable.toString() : null); + mPage.notifyDataChanged(); + } + }); + + mEmailView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, + int i2) { + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + } + + @Override + public void afterTextChanged(Editable editable) { + mPage.getData().putString(CustomerInfoPage.EMAIL_DATA_KEY, + (editable != null) ? editable.toString() : null); + mPage.notifyDataChanged(); + } + }); + } + + @Override + public void setMenuVisibility(boolean menuVisible) { + super.setMenuVisibility(menuVisible); + + // In a future update to the support library, this should override setUserVisibleHint + // instead of setMenuVisibility. + if (mNameView != null) { + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService( + Context.INPUT_METHOD_SERVICE); + if (!menuVisible) { + imm.hideSoftInputFromWindow(getView().getWindowToken(), 0); + } + } + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/ui/InstructionFragment.java b/app/src/main/java/com/markosullivan/wizards/wizard/ui/InstructionFragment.java new file mode 100644 index 0000000..6700476 --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/ui/InstructionFragment.java @@ -0,0 +1,96 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.markosullivan.wizards.R; +import com.markosullivan.wizards.wizard.model.InstructionPage; +import com.markosullivan.wizards.wizard.model.Page; + +import java.util.ArrayList; +import java.util.List; + +public class InstructionFragment extends Fragment { + private static final String ARG_KEY = "key"; + + private PageFragmentCallbacks mCallbacks; + private List mChoices; + private String mKey; + private Page mPage; + + public static InstructionFragment create(String key) { + Bundle args = new Bundle(); + args.putString(ARG_KEY, key); + + InstructionFragment fragment = new InstructionFragment(); + fragment.setArguments(args); + return fragment; + } + + public InstructionFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + mKey = args.getString(ARG_KEY); + mPage = mCallbacks.onGetPage(mKey); + + InstructionPage fixedChoicePage = (InstructionPage) mPage; + mChoices = new ArrayList(); + for (int i = 0; i < fixedChoicePage.getOptionCount(); i++) { + mChoices.add(fixedChoicePage.getOptionAt(i)); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_instruction, container, false); + ((TextView) rootView.findViewById(R.id.testingID)).setText("Help"); + + + return rootView; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (!(activity instanceof PageFragmentCallbacks)) { + throw new ClassCastException("Activity must implement PageFragmentCallbacks"); + } + + mCallbacks = (PageFragmentCallbacks) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + } + +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/ui/MultipleChoiceFragment.java b/app/src/main/java/com/markosullivan/wizards/wizard/ui/MultipleChoiceFragment.java new file mode 100644 index 0000000..4f94dbd --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/ui/MultipleChoiceFragment.java @@ -0,0 +1,141 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.ListFragment; +import android.util.SparseBooleanArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.markosullivan.wizards.R; +import com.markosullivan.wizards.wizard.model.MultipleFixedChoicePage; +import com.markosullivan.wizards.wizard.model.Page; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class MultipleChoiceFragment extends ListFragment { + private static final String ARG_KEY = "key"; + + private PageFragmentCallbacks mCallbacks; + private String mKey; + private List mChoices; + private Page mPage; + + public static MultipleChoiceFragment create(String key) { + Bundle args = new Bundle(); + args.putString(ARG_KEY, key); + + MultipleChoiceFragment fragment = new MultipleChoiceFragment(); + fragment.setArguments(args); + return fragment; + } + + public MultipleChoiceFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + mKey = args.getString(ARG_KEY); + mPage = mCallbacks.onGetPage(mKey); + + MultipleFixedChoicePage fixedChoicePage = (MultipleFixedChoicePage) mPage; + mChoices = new ArrayList(); + for (int i = 0; i < fixedChoicePage.getOptionCount(); i++) { + mChoices.add(fixedChoicePage.getOptionAt(i)); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_page, container, false); + ((TextView) rootView.findViewById(android.R.id.title)).setText(mPage.getTitle()); + + final ListView listView = (ListView) rootView.findViewById(android.R.id.list); + setListAdapter(new ArrayAdapter(getActivity(), + android.R.layout.simple_list_item_multiple_choice, + android.R.id.text1, + mChoices)); + listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + + // Pre-select currently selected items. + new Handler().post(new Runnable() { + @Override + public void run() { + ArrayList selectedItems = mPage.getData().getStringArrayList( + Page.SIMPLE_DATA_KEY); + if (selectedItems == null || selectedItems.size() == 0) { + return; + } + + Set selectedSet = new HashSet(selectedItems); + + for (int i = 0; i < mChoices.size(); i++) { + if (selectedSet.contains(mChoices.get(i))) { + listView.setItemChecked(i, true); + } + } + } + }); + + return rootView; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (!(activity instanceof PageFragmentCallbacks)) { + throw new ClassCastException("Activity must implement PageFragmentCallbacks"); + } + + mCallbacks = (PageFragmentCallbacks) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + SparseBooleanArray checkedPositions = getListView().getCheckedItemPositions(); + ArrayList selections = new ArrayList(); + for (int i = 0; i < checkedPositions.size(); i++) { + if (checkedPositions.valueAt(i)) { + selections.add(getListAdapter().getItem(checkedPositions.keyAt(i)).toString()); + } + } + + mPage.getData().putStringArrayList(Page.SIMPLE_DATA_KEY, selections); + mPage.notifyDataChanged(); + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/ui/PageFragmentCallbacks.java b/app/src/main/java/com/markosullivan/wizards/wizard/ui/PageFragmentCallbacks.java new file mode 100644 index 0000000..388ce8f --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/ui/PageFragmentCallbacks.java @@ -0,0 +1,21 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.ui; + +public interface PageFragmentCallbacks { + com.markosullivan.wizards.wizard.model.Page onGetPage(String key); +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/ui/ReviewFragment.java b/app/src/main/java/com/markosullivan/wizards/wizard/ui/ReviewFragment.java new file mode 100644 index 0000000..3adfe12 --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/ui/ReviewFragment.java @@ -0,0 +1,180 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.markosullivan.wizards.R; +import com.markosullivan.wizards.wizard.model.AbstractWizardModel; +import com.markosullivan.wizards.wizard.model.ModelCallbacks; +import com.markosullivan.wizards.wizard.model.Page; +import com.markosullivan.wizards.wizard.model.ReviewItem; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class ReviewFragment extends ListFragment implements ModelCallbacks { + private Callbacks mCallbacks; + private AbstractWizardModel mWizardModel; + private List mCurrentReviewItems; + + private ReviewAdapter mReviewAdapter; + + public ReviewFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mReviewAdapter = new ReviewAdapter(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_page, container, false); + + TextView titleView = (TextView) rootView.findViewById(android.R.id.title); + titleView.setText(R.string.review); + titleView.setTextColor(getResources().getColor(R.color.step_pager_selected_tab_color)); + + ListView listView = (ListView) rootView.findViewById(android.R.id.list); + setListAdapter(mReviewAdapter); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + return rootView; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (!(activity instanceof Callbacks)) { + throw new ClassCastException("Activity must implement fragment's callbacks"); + } + + mCallbacks = (Callbacks) activity; + + mWizardModel = mCallbacks.onGetModel(); + mWizardModel.registerListener(this); + onPageTreeChanged(); + } + + @Override + public void onPageTreeChanged() { + onPageDataChanged(null); + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + + mWizardModel.unregisterListener(this); + } + + @Override + public void onPageDataChanged(Page changedPage) { + ArrayList reviewItems = new ArrayList(); + for (Page page : mWizardModel.getCurrentPageSequence()) { + page.getReviewItems(reviewItems); + } + Collections.sort(reviewItems, new Comparator() { + @Override + public int compare(ReviewItem a, ReviewItem b) { + return a.getWeight() > b.getWeight() ? +1 : a.getWeight() < b.getWeight() ? -1 : 0; + } + }); + mCurrentReviewItems = reviewItems; + + if (mReviewAdapter != null) { + mReviewAdapter.notifyDataSetInvalidated(); + } + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + mCallbacks.onEditScreenAfterReview(mCurrentReviewItems.get(position).getPageKey()); + } + + public interface Callbacks { + AbstractWizardModel onGetModel(); + void onEditScreenAfterReview(String pageKey); + } + + private class ReviewAdapter extends BaseAdapter { + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public Object getItem(int position) { + return mCurrentReviewItems.get(position); + } + + @Override + public long getItemId(int position) { + return mCurrentReviewItems.get(position).hashCode(); + } + + @Override + public View getView(int position, View view, ViewGroup container) { + LayoutInflater inflater = LayoutInflater.from(getActivity()); + View rootView = inflater.inflate(R.layout.list_item_review, container, false); + + ReviewItem reviewItem = mCurrentReviewItems.get(position); + String value = reviewItem.getDisplayValue(); + if (TextUtils.isEmpty(value)) { + value = "(None)"; + } + ((TextView) rootView.findViewById(android.R.id.text1)).setText(reviewItem.getTitle()); + ((TextView) rootView.findViewById(android.R.id.text2)).setText(value); + return rootView; + } + + @Override + public int getCount() { + return mCurrentReviewItems.size(); + } + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/ui/SingleChoiceFragment.java b/app/src/main/java/com/markosullivan/wizards/wizard/ui/SingleChoiceFragment.java new file mode 100644 index 0000000..a0419cb --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/ui/SingleChoiceFragment.java @@ -0,0 +1,125 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.ListFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.markosullivan.wizards.R; +import com.markosullivan.wizards.wizard.model.Page; +import com.markosullivan.wizards.wizard.model.SingleFixedChoicePage; + +import java.util.ArrayList; +import java.util.List; + +public class SingleChoiceFragment extends ListFragment { + private static final String ARG_KEY = "key"; + + private PageFragmentCallbacks mCallbacks; + private List mChoices; + private String mKey; + private Page mPage; + + public static SingleChoiceFragment create(String key) { + Bundle args = new Bundle(); + args.putString(ARG_KEY, key); + + SingleChoiceFragment fragment = new SingleChoiceFragment(); + fragment.setArguments(args); + return fragment; + } + + public SingleChoiceFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + mKey = args.getString(ARG_KEY); + mPage = mCallbacks.onGetPage(mKey); + + SingleFixedChoicePage fixedChoicePage = (SingleFixedChoicePage) mPage; + mChoices = new ArrayList(); + for (int i = 0; i < fixedChoicePage.getOptionCount(); i++) { + mChoices.add(fixedChoicePage.getOptionAt(i)); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_page, container, false); + ((TextView) rootView.findViewById(android.R.id.title)).setText(mPage.getTitle()); + + final ListView listView = (ListView) rootView.findViewById(android.R.id.list); + setListAdapter(new ArrayAdapter(getActivity(), + android.R.layout.simple_list_item_single_choice, + android.R.id.text1, + mChoices)); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + + // Pre-select currently selected item. + new Handler().post(new Runnable() { + @Override + public void run() { + String selection = mPage.getData().getString(Page.SIMPLE_DATA_KEY); + for (int i = 0; i < mChoices.size(); i++) { + if (mChoices.get(i).equals(selection)) { + listView.setItemChecked(i, true); + break; + } + } + } + }); + + return rootView; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (!(activity instanceof PageFragmentCallbacks)) { + throw new ClassCastException("Activity must implement PageFragmentCallbacks"); + } + + mCallbacks = (PageFragmentCallbacks) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + mPage.getData().putString(Page.SIMPLE_DATA_KEY, + getListAdapter().getItem(position).toString()); + mPage.notifyDataChanged(); + } +} diff --git a/app/src/main/java/com/markosullivan/wizards/wizard/ui/StepPagerStrip.java b/app/src/main/java/com/markosullivan/wizards/wizard/ui/StepPagerStrip.java new file mode 100644 index 0000000..2d53afb --- /dev/null +++ b/app/src/main/java/com/markosullivan/wizards/wizard/ui/StepPagerStrip.java @@ -0,0 +1,270 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.markosullivan.wizards.wizard.ui; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; + +import com.markosullivan.wizards.R; + +public class StepPagerStrip extends View { + private static final int[] ATTRS = new int[]{ + android.R.attr.gravity + }; + private int mPageCount; + private int mCurrentPage; + + private int mGravity = Gravity.LEFT | Gravity.TOP; + private float mTabWidth; + private float mTabHeight; + private float mTabSpacing; + + private Paint mPrevTabPaint; + private Paint mSelectedTabPaint; + private Paint mSelectedLastTabPaint; + private Paint mNextTabPaint; + + private RectF mTempRectF = new RectF(); + + //private Scroller mScroller; + + private OnPageSelectedListener mOnPageSelectedListener; + + public StepPagerStrip(Context context) { + this(context, null, 0); + } + + public StepPagerStrip(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public StepPagerStrip(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); + mGravity = a.getInteger(0, mGravity); + a.recycle(); + + final Resources res = getResources(); + mTabWidth = res.getDimensionPixelSize(R.dimen.step_pager_tab_width); + mTabHeight = res.getDimensionPixelSize(R.dimen.step_pager_tab_height); + mTabSpacing = res.getDimensionPixelSize(R.dimen.step_pager_tab_spacing); + + mPrevTabPaint = new Paint(); + mPrevTabPaint.setColor(res.getColor(R.color.step_pager_previous_tab_color)); + + mSelectedTabPaint = new Paint(); + mSelectedTabPaint.setColor(res.getColor(R.color.step_pager_selected_tab_color)); + + mSelectedLastTabPaint = new Paint(); + mSelectedLastTabPaint.setColor(res.getColor(R.color.step_pager_selected_tab_color)); + + mNextTabPaint = new Paint(); + mNextTabPaint.setColor(res.getColor(R.color.step_pager_next_tab_color)); + } + + public void setOnPageSelectedListener(OnPageSelectedListener onPageSelectedListener) { + mOnPageSelectedListener = onPageSelectedListener; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mPageCount == 0) { + return; + } + + float totalWidth = mPageCount * (mTabWidth + mTabSpacing) - mTabSpacing; + float totalLeft; + boolean fillHorizontal = false; + + switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + totalLeft = (getWidth() - totalWidth) / 2; + break; + case Gravity.RIGHT: + totalLeft = getWidth() - getPaddingRight() - totalWidth; + break; + case Gravity.FILL_HORIZONTAL: + totalLeft = getPaddingLeft(); + fillHorizontal = true; + break; + default: + totalLeft = getPaddingLeft(); + } + + switch (mGravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.CENTER_VERTICAL: + mTempRectF.top = (int) (getHeight() - mTabHeight) / 2; + break; + case Gravity.BOTTOM: + mTempRectF.top = getHeight() - getPaddingBottom() - mTabHeight; + break; + default: + mTempRectF.top = getPaddingTop(); + } + + mTempRectF.bottom = mTempRectF.top + mTabHeight; + + float tabWidth = mTabWidth; + if (fillHorizontal) { + tabWidth = (getWidth() - getPaddingRight() - getPaddingLeft() + - (mPageCount - 1) * mTabSpacing) / mPageCount; + } + + for (int i = 0; i < mPageCount; i++) { + mTempRectF.left = totalLeft + (i * (tabWidth + mTabSpacing)); + mTempRectF.right = mTempRectF.left + tabWidth; + canvas.drawRect(mTempRectF, i < mCurrentPage + ? mPrevTabPaint + : (i > mCurrentPage + ? mNextTabPaint + : (i == mPageCount - 1 + ? mSelectedLastTabPaint + : mSelectedTabPaint))); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension( + View.resolveSize( + (int) (mPageCount * (mTabWidth + mTabSpacing) - mTabSpacing) + + getPaddingLeft() + getPaddingRight(), + widthMeasureSpec), + View.resolveSize( + (int) mTabHeight + + getPaddingTop() + getPaddingBottom(), + heightMeasureSpec)); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + scrollCurrentPageIntoView(); + super.onSizeChanged(w, h, oldw, oldh); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mOnPageSelectedListener != null) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + int position = hitTest(event.getX()); + if (position >= 0) { + mOnPageSelectedListener.onPageStripSelected(position); + } + return true; + } + } + return super.onTouchEvent(event); + } + + private int hitTest(float x) { + if (mPageCount == 0) { + return -1; + } + + float totalWidth = mPageCount * (mTabWidth + mTabSpacing) - mTabSpacing; + float totalLeft; + boolean fillHorizontal = false; + + switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + totalLeft = (getWidth() - totalWidth) / 2; + break; + case Gravity.RIGHT: + totalLeft = getWidth() - getPaddingRight() - totalWidth; + break; + case Gravity.FILL_HORIZONTAL: + totalLeft = getPaddingLeft(); + fillHorizontal = true; + break; + default: + totalLeft = getPaddingLeft(); + } + + float tabWidth = mTabWidth; + if (fillHorizontal) { + tabWidth = (getWidth() - getPaddingRight() - getPaddingLeft() + - (mPageCount - 1) * mTabSpacing) / mPageCount; + } + + float totalRight = totalLeft + (mPageCount * (tabWidth + mTabSpacing)); + if (x >= totalLeft && x <= totalRight && totalRight > totalLeft) { + return (int) (((x - totalLeft) / (totalRight - totalLeft)) * mPageCount); + } else { + return -1; + } + } + + public void setCurrentPage(int currentPage) { + mCurrentPage = currentPage; + invalidate(); + scrollCurrentPageIntoView(); + + // TODO: Set content description appropriately + } + + private void scrollCurrentPageIntoView() { + // TODO: only works with left gravity for now +// +// float widthToActive = getPaddingLeft() + (mCurrentPage + 1) * (mTabWidth + mTabSpacing) +// - mTabSpacing; +// int viewWidth = getWidth(); +// +// int startScrollX = getScrollX(); +// int destScrollX = (widthToActive > viewWidth) ? (int) (widthToActive - viewWidth) : 0; +// +// if (mScroller == null) { +// mScroller = new Scroller(getContext()); +// } +// +// mScroller.abortAnimation(); +// mScroller.startScroll(startScrollX, 0, destScrollX - startScrollX, 0); +// postInvalidate(); + } + + public void setPageCount(int count) { + mPageCount = count; + invalidate(); + + // TODO: Set content description appropriately + } + + public static interface OnPageSelectedListener { + void onPageStripSelected(int position); + } + +// +// @Override +// public void computeScroll() { +// super.computeScroll(); +// if (mScroller.computeScrollOffset()) { +// setScrollX(mScroller.getCurrX()); +// } +// } +} diff --git a/app/src/main/res/drawable/finish_background.xml b/app/src/main/res/drawable/finish_background.xml new file mode 100644 index 0000000..81789e4 --- /dev/null +++ b/app/src/main/res/drawable/finish_background.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/selectable_item_background.xml b/app/src/main/res/drawable/selectable_item_background.xml new file mode 100644 index 0000000..718995f --- /dev/null +++ b/app/src/main/res/drawable/selectable_item_background.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..6a42e7b --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,55 @@ + + + + + + + + + + +