diff --git a/res/layout/fragment_wizard_page.xml b/res/layout/fragment_wizard_page.xml new file mode 100644 index 0000000000000000000000000000000000000000..cbc7186b417e27cc8b5785b7eae3402f1d3c7c6c --- /dev/null +++ b/res/layout/fragment_wizard_page.xml @@ -0,0 +1,31 @@ +<!-- + 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. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + style="@style/WizardPageContainer"> + + <TextView style="@style/WizardPageTitle" /> + + <ListView android:id="@android:id/list" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:saveEnabled="false" + android:scrollbarStyle="outsideOverlay" /> + +</LinearLayout> diff --git a/res/layout/listitem_wizard_review.xml b/res/layout/listitem_wizard_review.xml new file mode 100644 index 0000000000000000000000000000000000000000..c0fe3a724cb8b682c859075c1706737c6f18393e --- /dev/null +++ b/res/layout/listitem_wizard_review.xml @@ -0,0 +1,40 @@ +<!-- + 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. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:baselineAligned="true" + android:paddingTop="12dp" + android:paddingBottom="12dp"> + + <TextView android:id="@android:id/text1" + style="?android:textAppearanceSmall" + android:textAllCaps="true" + android:textStyle="bold" + android:ellipsize="end" + android:layout_width="100sp" + android:layout_height="wrap_content" + android:layout_marginRight="16dp" /> + + <TextView android:id="@android:id/text2" + style="?android:textAppearanceMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" /> + +</LinearLayout> diff --git a/res/values/colors.xml b/res/values/colors.xml new file mode 100644 index 0000000000000000000000000000000000000000..bf983966b44b4e1b36069888aa73ec54a471a1f0 --- /dev/null +++ b/res/values/colors.xml @@ -0,0 +1,8 @@ +<resources> + <color name="step_pager_previous_tab_color">#4433b5e5</color> + <color name="step_pager_selected_tab_color">#ff0099cc</color> + <color name="step_pager_selected_last_tab_color">#ff669900</color> + <color name="step_pager_next_tab_color">#10000000</color> + + <color name="review_green">#ff669900</color> +</resources> diff --git a/res/values/dimens.xml b/res/values/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..b3bf632b3339cfd866ceaec4e24bc08b8c0aa515 --- /dev/null +++ b/res/values/dimens.xml @@ -0,0 +1,5 @@ +<resources> + <dimen name="step_pager_tab_width">32dp</dimen> + <dimen name="step_pager_tab_height">3dp</dimen> + <dimen name="step_pager_tab_spacing">4dp</dimen> +</resources> diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100644 index 0000000000000000000000000000000000000000..b35b21f8f7945b0cf507363cf9044c271af6d9ff --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,29 @@ +<resources> + <style name="WizardPageContainer"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">match_parent</item> + <item name="android:orientation">vertical</item> + </style> + + <style name="WizardPageTitle"> + <item name="android:id">@android:id/title</item> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_marginBottom">8dp</item> + <item name="android:layout_marginLeft">16dp</item> + <item name="android:layout_marginRight">16dp</item> + <item name="android:paddingLeft">?android:attr/listPreferredItemPaddingLeft</item> + <item name="android:textSize">36sp</item> + <item name="android:textColor">#ff0099cc</item> + </style> + + <style name="WizardFormLabel"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_marginBottom">0dp</item> + <item name="android:textAppearance">?android:textAppearanceSmall</item> + <item name="android:textStyle">bold</item> + <item name="android:paddingLeft">12dp</item> + <item name="android:paddingRight">12dp</item> + </style> +</resources> diff --git a/src/net/i2p/android/wizard/model/AbstractWizardModel.java b/src/net/i2p/android/wizard/model/AbstractWizardModel.java new file mode 100644 index 0000000000000000000000000000000000000000..843b382e415a0a1feede54072d37af4c428ef74e --- /dev/null +++ b/src/net/i2p/android/wizard/model/AbstractWizardModel.java @@ -0,0 +1,99 @@ +/* + * Copyright 2013 Google Inc. + * Copyright 2013 str4d + * + * 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 net.i2p.android.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<ModelCallbacks> mListeners = new ArrayList<ModelCallbacks>(); + private PageList mRootPageList; + + public AbstractWizardModel(Context context) { + mContext = context; + mRootPageList = onNewRootPageList(); + } + + /** + * Override this to define a new wizard model. + */ + protected abstract PageList onNewRootPageList(); + + 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); + } + } + + 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<Page> getCurrentPageSequence() { + ArrayList<Page> flattened = new ArrayList<Page>(); + mRootPageList.flattenCurrentPageSequence(flattened); + return flattened; + } + + public void unregisterListener(ModelCallbacks listener) { + mListeners.remove(listener); + } +} diff --git a/src/net/i2p/android/wizard/model/BranchPage.java b/src/net/i2p/android/wizard/model/BranchPage.java new file mode 100644 index 0000000000000000000000000000000000000000..4feb56c9f84003fb1f9800d997e5ee80cd0e32a9 --- /dev/null +++ b/src/net/i2p/android/wizard/model/BranchPage.java @@ -0,0 +1,117 @@ +/* + * 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 net.i2p.android.wizard.model; + +import net.i2p.android.wizard.ui.SingleChoiceFragment; + +import android.support.v4.app.Fragment; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * 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<Branch> mBranches = new ArrayList<Branch>(); + + 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<Page> 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; + } + + @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<ReviewItem> 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/src/net/i2p/android/wizard/model/ModelCallbacks.java b/src/net/i2p/android/wizard/model/ModelCallbacks.java new file mode 100644 index 0000000000000000000000000000000000000000..baddecf9b15e8e4a795a9bd2d2b88e8eb07c4c58 --- /dev/null +++ b/src/net/i2p/android/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 net.i2p.android.wizard.model; + +/** + * Callback interface connecting {@link Page}, {@link AbstractWizardModel}, and model container + * objects (e.g. {@link net.i2p.android.i2ptunnel.activity.TunnelWizardActivity}. + */ +public interface ModelCallbacks { + void onPageDataChanged(Page page); + void onPageTreeChanged(); +} diff --git a/src/net/i2p/android/wizard/model/MultipleFixedChoicePage.java b/src/net/i2p/android/wizard/model/MultipleFixedChoicePage.java new file mode 100644 index 0000000000000000000000000000000000000000..9ff5d748524219d22d4808b6027d25d4d5790951 --- /dev/null +++ b/src/net/i2p/android/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 net.i2p.android.wizard.model; + +import net.i2p.android.wizard.ui.MultipleChoiceFragment; + +import android.support.v4.app.Fragment; + +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<ReviewItem> dest) { + StringBuilder sb = new StringBuilder(); + + ArrayList<String> 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<String> selections = mData.getStringArrayList(Page.SIMPLE_DATA_KEY); + return selections != null && selections.size() > 0; + } +} diff --git a/src/net/i2p/android/wizard/model/Page.java b/src/net/i2p/android/wizard/model/Page.java new file mode 100644 index 0000000000000000000000000000000000000000..0135f3312ff0e8bc3cc8e18f51f2641517842209 --- /dev/null +++ b/src/net/i2p/android/wizard/model/Page.java @@ -0,0 +1,98 @@ +/* + * Copyright 2013 Google Inc. + * Copyright 2013 str4d + * + * 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 net.i2p.android.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; + } + + public Page findByKey(String key) { + return getKey().equals(key) ? this : null; + } + + public void flattenCurrentPageSequence(ArrayList<Page> dest) { + dest.add(this); + } + + public abstract Fragment createFragment(); + + public String getKey() { + return (mParentKey != null) ? mParentKey + ":" + mTitle : mTitle; + } + + public abstract void getReviewItems(ArrayList<ReviewItem> 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/src/net/i2p/android/wizard/model/PageList.java b/src/net/i2p/android/wizard/model/PageList.java new file mode 100644 index 0000000000000000000000000000000000000000..27742b887d660bd22507d596221137bd18a7ae4b --- /dev/null +++ b/src/net/i2p/android/wizard/model/PageList.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013 Google Inc. + * Copyright 2013 str4d + * + * 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 net.i2p.android.wizard.model; + +import java.util.ArrayList; + +/** + * Represents a list of wizard pages. + */ +public class PageList extends ArrayList<Page> implements PageTreeNode { + public PageList(Page... pages) { + for (Page page : pages) { + add(page); + } + } + + public Page findByKey(String key) { + for (Page childPage : this) { + Page found = childPage.findByKey(key); + if (found != null) { + return found; + } + } + + return null; + } + + public void flattenCurrentPageSequence(ArrayList<Page> dest) { + for (Page childPage : this) { + childPage.flattenCurrentPageSequence(dest); + } + } +} diff --git a/src/net/i2p/android/wizard/model/PageTreeNode.java b/src/net/i2p/android/wizard/model/PageTreeNode.java new file mode 100644 index 0000000000000000000000000000000000000000..49d595de13c0efc1384a467187066613aa2697f6 --- /dev/null +++ b/src/net/i2p/android/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 net.i2p.android.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<Page> dest); +} diff --git a/src/net/i2p/android/wizard/model/ReviewItem.java b/src/net/i2p/android/wizard/model/ReviewItem.java new file mode 100644 index 0000000000000000000000000000000000000000..d3c00f6b316dec0d218895fb708618cbac19edb6 --- /dev/null +++ b/src/net/i2p/android/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 net.i2p.android.wizard.model; + +/** + * Represents a single line item on the final review page. + * + * @see net.i2p.android.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/src/net/i2p/android/wizard/model/SingleFixedChoicePage.java b/src/net/i2p/android/wizard/model/SingleFixedChoicePage.java new file mode 100644 index 0000000000000000000000000000000000000000..64bb4ea0505e2730388cf38206e2a1a79ea77f26 --- /dev/null +++ b/src/net/i2p/android/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 net.i2p.android.wizard.model; + +import net.i2p.android.wizard.ui.SingleChoiceFragment; + +import android.support.v4.app.Fragment; +import android.text.TextUtils; + +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<String> mChoices = new ArrayList<String>(); + + 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<ReviewItem> 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/src/net/i2p/android/wizard/ui/MultipleChoiceFragment.java b/src/net/i2p/android/wizard/ui/MultipleChoiceFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..05e6e458c7d687712cab772ff9187815760d2155 --- /dev/null +++ b/src/net/i2p/android/wizard/ui/MultipleChoiceFragment.java @@ -0,0 +1,141 @@ +/* + * Copyright 2013 Google Inc. + * Copyright 2013 str4d + * + * 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 net.i2p.android.wizard.ui; + +import net.i2p.android.router.R; +import net.i2p.android.wizard.model.MultipleFixedChoicePage; +import net.i2p.android.wizard.model.Page; + +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 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<String> 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<String>(); + 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_wizard_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<String>(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() { + public void run() { + ArrayList<String> selectedItems = mPage.getData().getStringArrayList( + Page.SIMPLE_DATA_KEY); + if (selectedItems == null || selectedItems.size() == 0) { + return; + } + + Set<String> selectedSet = new HashSet<String>(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<String> selections = new ArrayList<String>(); + 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/src/net/i2p/android/wizard/ui/PageFragmentCallbacks.java b/src/net/i2p/android/wizard/ui/PageFragmentCallbacks.java new file mode 100644 index 0000000000000000000000000000000000000000..b0cfae019e7c628627282dd7129d969d59f5db2d --- /dev/null +++ b/src/net/i2p/android/wizard/ui/PageFragmentCallbacks.java @@ -0,0 +1,23 @@ +/* + * 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 net.i2p.android.wizard.ui; + +import net.i2p.android.wizard.model.Page; + +public interface PageFragmentCallbacks { + Page onGetPage(String key); +} diff --git a/src/net/i2p/android/wizard/ui/ReviewFragment.java b/src/net/i2p/android/wizard/ui/ReviewFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..d956476d79c49447c1084c9702b09ac9933eb17a --- /dev/null +++ b/src/net/i2p/android/wizard/ui/ReviewFragment.java @@ -0,0 +1,174 @@ +/* + * Copyright 2013 Google Inc. + * Copyright 2013 str4d + * + * 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 net.i2p.android.wizard.ui; + +import net.i2p.android.router.R; +import net.i2p.android.wizard.model.AbstractWizardModel; +import net.i2p.android.wizard.model.ModelCallbacks; +import net.i2p.android.wizard.model.Page; +import net.i2p.android.wizard.model.ReviewItem; + +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 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<ReviewItem> 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_wizard_page, container, false); + + TextView titleView = (TextView) rootView.findViewById(android.R.id.title); + titleView.setText(R.string.review); + titleView.setTextColor(getResources().getColor(R.color.review_green)); + + 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(); + } + + public void onPageTreeChanged() { + onPageDataChanged(null); + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + + mWizardModel.unregisterListener(this); + } + + public void onPageDataChanged(Page changedPage) { + ArrayList<ReviewItem> reviewItems = new ArrayList<ReviewItem>(); + for (Page page : mWizardModel.getCurrentPageSequence()) { + page.getReviewItems(reviewItems); + } + Collections.sort(reviewItems, new Comparator<ReviewItem>() { + 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; + } + + public Object getItem(int position) { + return mCurrentReviewItems.get(position); + } + + public long getItemId(int position) { + return mCurrentReviewItems.get(position).hashCode(); + } + + public View getView(int position, View view, ViewGroup container) { + LayoutInflater inflater = LayoutInflater.from(getActivity()); + View rootView = inflater.inflate(R.layout.listitem_wizard_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; + } + + public int getCount() { + return mCurrentReviewItems.size(); + } + } +} diff --git a/src/net/i2p/android/wizard/ui/SingleChoiceFragment.java b/src/net/i2p/android/wizard/ui/SingleChoiceFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..93a419c6748a5029ff035307e4918d4cc6926cd2 --- /dev/null +++ b/src/net/i2p/android/wizard/ui/SingleChoiceFragment.java @@ -0,0 +1,125 @@ +/* + * Copyright 2013 Google Inc. + * Copyright 2013 str4d + * + * 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 net.i2p.android.wizard.ui; + +import net.i2p.android.router.R; +import net.i2p.android.wizard.model.Page; +import net.i2p.android.wizard.model.SingleFixedChoicePage; + +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 java.util.ArrayList; +import java.util.List; + +public class SingleChoiceFragment extends ListFragment { + private static final String ARG_KEY = "key"; + + private PageFragmentCallbacks mCallbacks; + private List<String> 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<String>(); + 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_wizard_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<String>(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() { + 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/src/net/i2p/android/wizard/ui/StepPagerStrip.java b/src/net/i2p/android/wizard/ui/StepPagerStrip.java new file mode 100644 index 0000000000000000000000000000000000000000..40c35dce7cadba4ca44e5b14059e8c3d8a79f0fe --- /dev/null +++ b/src/net/i2p/android/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 net.i2p.android.wizard.ui; + +import net.i2p.android.router.R; + +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; + +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_last_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()); +// } +// } +}