matches = data
121 | .getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
122 | mSearchView.populateEditText(matches); // Set result to PersistentSearchView
123 | }
124 | super.onActivityResult(requestCode, resultCode, data);
125 | }
126 | ```
127 |
128 | If you don't want to use default voice recognizer, you could inherit from abstract class VoiceRecognitionDelegate and implement your own recognizer.
129 |
130 | ## Screenshot
131 |
132 | 
133 | 
134 |
135 | ## Thanks
136 |
137 | - The project originally came from [Quinny898/PersistentSearch](https://github.com/Quinny898/PersistentSearch).
138 | - [Ozodrukh/CircularReveal](https://github.com/ozodrukh/CircularReveal) for reveal animation.
139 | - [kee23](https://github.com/kee23)
140 |
141 | ## License
142 |
143 | Copyright 2015 Cryse Hillmes
144 |
145 | Licensed under the Apache License, Version 2.0 (the "License");
146 | you may not use this file except in compliance with the License.
147 | You may obtain a copy of the License at
148 |
149 | http://www.apache.org/licenses/LICENSE-2.0
150 |
151 | Unless required by applicable law or agreed to in writing, software
152 | distributed under the License is distributed on an "AS IS" BASIS,
153 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
154 | See the License for the specific language governing permissions and
155 | limitations under the License.
156 |
--------------------------------------------------------------------------------
/persistentsearchview/src/main/java/io/codetail/animation/ViewAnimationUtils.java:
--------------------------------------------------------------------------------
1 | package io.codetail.animation;
2 |
3 | import android.animation.Animator;
4 | import android.animation.ObjectAnimator;
5 | import android.annotation.TargetApi;
6 | import android.os.Build;
7 | import android.view.View;
8 | import android.view.animation.AccelerateDecelerateInterpolator;
9 |
10 | import java.lang.ref.WeakReference;
11 |
12 | import io.codetail.animation.RevealAnimator.RevealInfo;
13 |
14 | import static android.os.Build.VERSION.SDK_INT;
15 | import static android.os.Build.VERSION_CODES.LOLLIPOP;
16 | import static io.codetail.animation.RevealAnimator.CLIP_RADIUS;
17 |
18 | public class ViewAnimationUtils {
19 |
20 | private final static boolean LOLLIPOP_PLUS = SDK_INT >= LOLLIPOP;
21 |
22 | public static final int SCALE_UP_DURATION = 500;
23 |
24 | /**
25 | * Returns an Animator which can animate a clipping circle.
26 | *
27 | * Any shadow cast by the View will respect the circular clip from this animator.
28 | *
29 | * Only a single non-rectangular clip can be applied on a View at any time.
30 | * Views clipped by a circular reveal animation take priority over
31 | * {@link android.view.View#setClipToOutline(boolean) View Outline clipping}.
32 | *
33 | * Note that the animation returned here is a one-shot animation. It cannot
34 | * be re-used, and once started it cannot be paused or resumed.
35 | *
36 | * @param view The View will be clipped to the animating circle.
37 | * @param centerX The x coordinate of the center of the animating circle.
38 | * @param centerY The y coordinate of the center of the animating circle.
39 | * @param startRadius The starting radius of the animating circle.
40 | * @param endRadius The ending radius of the animating circle.
41 | */
42 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
43 | public static SupportAnimator createCircularReveal(View view,
44 | int centerX, int centerY,
45 | float startRadius, float endRadius) {
46 |
47 | if(!(view.getParent() instanceof RevealAnimator)){
48 | throw new IllegalArgumentException("View must be inside RevealFrameLayout or RevealLinearLayout.");
49 | }
50 |
51 | RevealAnimator revealLayout = (RevealAnimator) view.getParent();
52 | revealLayout.attachRevealInfo(new RevealInfo(centerX, centerY, startRadius, endRadius,
53 | new WeakReference<>(view)));
54 |
55 | if(LOLLIPOP_PLUS){
56 | return new SupportAnimatorLollipop(android.view.ViewAnimationUtils
57 | .createCircularReveal(view, centerX, centerY, startRadius, endRadius), revealLayout);
58 | }
59 |
60 | ObjectAnimator reveal = ObjectAnimator.ofFloat(revealLayout, CLIP_RADIUS,
61 | startRadius, endRadius);
62 | reveal.addListener(getRevealFinishListener(revealLayout));
63 |
64 | return new SupportAnimatorPreL(reveal, revealLayout);
65 | }
66 |
67 | private static Animator.AnimatorListener getRevealFinishListener(RevealAnimator target){
68 | if(SDK_INT >= 18){
69 | return new RevealAnimator.RevealFinishedJellyBeanMr2(target);
70 | }else if(SDK_INT >= 14){
71 | return new RevealAnimator.RevealFinishedIceCreamSandwich(target);
72 | }else {
73 | return new RevealAnimator.RevealFinishedGingerbread(target);
74 | }
75 | }
76 |
77 | /**
78 | * Lifting view
79 | *
80 | * @param view The animation target
81 | * @param baseRotation initial Rotation X in 3D space
82 | * @param fromY initial Y position of view
83 | * @param duration aniamtion duration
84 | * @param startDelay start delay before animation begin
85 | */
86 | @Deprecated
87 | public static void liftingFromBottom(View view, float baseRotation, float fromY, int duration, int startDelay){
88 | view.setRotationX(baseRotation);
89 | view.setTranslationY(fromY);
90 |
91 | view
92 | .animate()
93 | .setInterpolator(new AccelerateDecelerateInterpolator())
94 | .setDuration(duration)
95 | .setStartDelay(startDelay)
96 | .rotationX(0)
97 | .translationY(0)
98 | .start();
99 |
100 | }
101 |
102 | /**
103 | * Lifting view
104 | *
105 | * @param view The animation target
106 | * @param baseRotation initial Rotation X in 3D space
107 | * @param duration aniamtion duration
108 | * @param startDelay start delay before animation begin
109 | */
110 | @Deprecated
111 | public static void liftingFromBottom(View view, float baseRotation, int duration, int startDelay){
112 | view.setRotationX(baseRotation);
113 | view.setTranslationY(view.getHeight() / 3);
114 |
115 | view
116 | .animate()
117 | .setInterpolator(new AccelerateDecelerateInterpolator())
118 | .setDuration(duration)
119 | .setStartDelay(startDelay)
120 | .rotationX(0)
121 | .translationY(0)
122 | .start();
123 |
124 | }
125 |
126 | /**
127 | * Lifting view
128 | *
129 | * @param view The animation target
130 | * @param baseRotation initial Rotation X in 3D space
131 | * @param duration aniamtion duration
132 | */
133 | @Deprecated
134 | public static void liftingFromBottom(View view, float baseRotation, int duration){
135 | view.setRotationX(baseRotation);
136 | view.setTranslationY(view.getHeight() / 3);
137 |
138 | view
139 | .animate()
140 | .setInterpolator(new AccelerateDecelerateInterpolator())
141 | .setDuration(duration)
142 | .rotationX(0)
143 | .translationY(0)
144 | .start();
145 |
146 | }
147 |
148 | static class SimpleAnimationListener implements Animator.AnimatorListener{
149 |
150 | @Override
151 | public void onAnimationStart(Animator animation) {
152 |
153 | }
154 |
155 | @Override
156 | public void onAnimationEnd(Animator animation) {
157 |
158 | }
159 |
160 | @Override
161 | public void onAnimationCancel(Animator animation) {
162 |
163 | }
164 |
165 | @Override
166 | public void onAnimationRepeat(Animator animation) {
167 |
168 | }
169 | }
170 |
171 | }
--------------------------------------------------------------------------------
/sample/src/main/java/org/cryse/widget/persistentsearch/sample/SearchActivity.java:
--------------------------------------------------------------------------------
1 | package org.cryse.widget.persistentsearch.sample;
2 |
3 | import android.animation.Animator;
4 | import android.app.Activity;
5 | import android.content.Intent;
6 | import android.os.Bundle;
7 | import android.speech.RecognizerIntent;
8 | import android.support.v7.widget.DefaultItemAnimator;
9 | import android.support.v7.widget.LinearLayoutManager;
10 | import android.support.v7.widget.RecyclerView;
11 | import android.util.Log;
12 | import android.view.View;
13 | import android.widget.Toast;
14 |
15 | import org.cryse.widget.persistentsearch.DefaultVoiceRecognizerDelegate;
16 | import org.cryse.widget.persistentsearch.PersistentSearchView;
17 | import org.cryse.widget.persistentsearch.PersistentSearchView.HomeButtonListener;
18 | import org.cryse.widget.persistentsearch.PersistentSearchView.SearchListener;
19 | import org.cryse.widget.persistentsearch.SearchItem;
20 | import org.cryse.widget.persistentsearch.VoiceRecognitionDelegate;
21 |
22 | import java.util.ArrayList;
23 | import java.util.List;
24 |
25 | public class SearchActivity extends Activity {
26 | private static final int VOICE_RECOGNITION_REQUEST_CODE = 1023;
27 | private PersistentSearchView mSearchView;
28 | private View mSearchTintView;
29 | private SearchResultAdapter mResultAdapter;
30 | private RecyclerView mRecyclerView;
31 | @Override
32 | protected void onCreate(Bundle savedInstanceState) {
33 | super.onCreate(savedInstanceState);
34 | setContentView(R.layout.activity_search);
35 | mSearchView = (PersistentSearchView) findViewById(R.id.searchview);
36 | mSearchTintView = findViewById(R.id.view_search_tint);
37 | mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview_search_result);
38 | mRecyclerView.setItemAnimator(new DefaultItemAnimator());
39 | mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
40 | mResultAdapter = new SearchResultAdapter(new ArrayList());
41 | mRecyclerView.setAdapter(mResultAdapter);
42 | VoiceRecognitionDelegate delegate = new DefaultVoiceRecognizerDelegate(this, VOICE_RECOGNITION_REQUEST_CODE);
43 | if(delegate.isVoiceRecognitionAvailable()) {
44 | mSearchView.setVoiceRecognitionDelegate(delegate);
45 | }
46 | // mSearchView.openSearch("Text Query");
47 | mSearchView.setHomeButtonListener(new HomeButtonListener() {
48 |
49 | @Override
50 | public void onHomeButtonClick() {
51 | //Hamburger has been clicked
52 | finish();
53 | }
54 |
55 | });
56 | mSearchTintView.setOnClickListener(new View.OnClickListener() {
57 | @Override
58 | public void onClick(View v) {
59 | mSearchView.cancelEditing();
60 | }
61 | });
62 | mSearchView.setSuggestionBuilder(new SampleSuggestionsBuilder(this));
63 | mSearchView.setSearchListener(new SearchListener() {
64 |
65 | @Override
66 | public boolean onSuggestion(SearchItem searchItem) {
67 | Log.d("onSuggestion", searchItem.getTitle());
68 | return false;
69 | }
70 |
71 | @Override
72 | public void onSearchEditOpened() {
73 | //Use this to tint the screen
74 | mSearchTintView.setVisibility(View.VISIBLE);
75 | mSearchTintView
76 | .animate()
77 | .alpha(1.0f)
78 | .setDuration(300)
79 | .setListener(new SimpleAnimationListener())
80 | .start();
81 | }
82 |
83 | @Override
84 | public void onSearchEditClosed() {
85 | //Use this to un-tint the screen
86 | mSearchTintView
87 | .animate()
88 | .alpha(0.0f)
89 | .setDuration(300)
90 | .setListener(new SimpleAnimationListener() {
91 | @Override
92 | public void onAnimationEnd(Animator animation) {
93 | super.onAnimationEnd(animation);
94 | mSearchTintView.setVisibility(View.GONE);
95 | }
96 | })
97 | .start();
98 | }
99 |
100 | @Override
101 | public boolean onSearchEditBackPressed() {
102 | return false;
103 | }
104 |
105 | @Override
106 | public void onSearchExit() {
107 | mResultAdapter.clear();
108 | if (mRecyclerView.getVisibility() == View.VISIBLE) {
109 | mRecyclerView.setVisibility(View.GONE);
110 | }
111 | }
112 |
113 | @Override
114 | public void onSearchTermChanged(String term) {
115 |
116 | }
117 |
118 | @Override
119 | public void onSearch(String string) {
120 | Toast.makeText(SearchActivity.this, string + " Searched", Toast.LENGTH_LONG).show();
121 | mRecyclerView.setVisibility(View.VISIBLE);
122 | fillResultToRecyclerView(string);
123 | }
124 |
125 | @Override
126 | public void onSearchCleared() {
127 | //Called when the clear button is clicked
128 | }
129 |
130 | });
131 | mSearchView.openSearch("John von Neumann");
132 | }
133 |
134 | @Override
135 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
136 | if (requestCode == VOICE_RECOGNITION_REQUEST_CODE && resultCode == RESULT_OK) {
137 | ArrayList matches = data
138 | .getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
139 | mSearchView.populateEditText(matches);
140 | }
141 | super.onActivityResult(requestCode, resultCode, data);
142 | }
143 |
144 | private void fillResultToRecyclerView(String query) {
145 | List newResults = new ArrayList<>();
146 | for(int i =0; i< 10; i++) {
147 | SearchResult result = new SearchResult(query, query + Integer.toString(i), "");
148 | newResults.add(result);
149 | }
150 | mResultAdapter.replaceWith(newResults);
151 | }
152 |
153 | @Override
154 | public void onBackPressed() {
155 | if(mSearchView.isEditing()) {
156 | mSearchView.cancelEditing();
157 | } else {
158 | super.onBackPressed();
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/persistentsearchview/src/main/java/io/codetail/animation/SupportAnimator.java:
--------------------------------------------------------------------------------
1 | package io.codetail.animation;
2 |
3 | import android.view.animation.Interpolator;
4 |
5 | import java.lang.ref.WeakReference;
6 |
7 | public abstract class SupportAnimator {
8 |
9 | WeakReference mTarget;
10 |
11 | public SupportAnimator(RevealAnimator target) {
12 | mTarget = new WeakReference<>(target);
13 | }
14 |
15 | /**
16 | * @return true if using native android animation framework, otherwise is
17 | * nineoldandroids
18 | */
19 | public abstract boolean isNativeAnimator();
20 |
21 | /**
22 | * @return depends from {@link android.os.Build.VERSION} if sdk version
23 | * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and greater will return
24 | * {@link android.animation.Animator}
25 | */
26 | public abstract Object get();
27 |
28 | /**
29 | * Starts this animation. If the animation has a nonzero startDelay, the animation will start
30 | * running after that delay elapses. A non-delayed animation will have its initial
31 | * value(s) set immediately, followed by calls to
32 | * {@link android.animation.Animator.AnimatorListener#onAnimationStart(android.animation.Animator)}
33 | * for any listeners of this animator.
34 | *
35 | * The animation started by calling this method will be run on the thread that called
36 | * this method. This thread should have a Looper on it (a runtime exception will be thrown if
37 | * this is not the case). Also, if the animation will animate
38 | * properties of objects in the view hierarchy, then the calling thread should be the UI
39 | * thread for that view hierarchy.
40 | *
41 | */
42 | public abstract void start();
43 |
44 | /**
45 | * Sets the duration of the animation.
46 | *
47 | * @param duration The length of the animation, in milliseconds.
48 | */
49 | public abstract void setDuration(int duration);
50 |
51 | /**
52 | * The time interpolator used in calculating the elapsed fraction of the
53 | * animation. The interpolator determines whether the animation runs with
54 | * linear or non-linear motion, such as acceleration and deceleration. The
55 | * default value is {@link android.view.animation.AccelerateDecelerateInterpolator}.
56 | *
57 | * @param value the interpolator to be used by this animation
58 | */
59 | public abstract void setInterpolator(Interpolator value);
60 |
61 |
62 | /**
63 | * Adds a listener to the set of listeners that are sent events through the life of an
64 | * animation, such as start, repeat, and end.
65 | *
66 | * @param listener the listener to be added to the current set of listeners for this animation.
67 | */
68 | public abstract void addListener(AnimatorListener listener);
69 |
70 |
71 | /**
72 | * Returns whether this Animator is currently running (having been started and gone past any
73 | * initial startDelay period and not yet ended).
74 | *
75 | * @return Whether the Animator is running.
76 | */
77 | public abstract boolean isRunning();
78 |
79 |
80 | /**
81 | * Cancels the animation. Unlike {@link #end()}, cancel() causes the animation to
82 | * stop in its tracks, sending an
83 | * {@link AnimatorListener#onAnimationCancel()} to
84 | * its listeners, followed by an
85 | * {@link AnimatorListener#onAnimationEnd()} message.
86 | *
87 | * This method must be called on the thread that is running the animation.
88 | */
89 | public abstract void cancel();
90 |
91 | /**
92 | * Ends the animation. This causes the animation to assign the end value of the property being
93 | * animated, then calling the
94 | * {@link AnimatorListener#onAnimationEnd()} method on
95 | * its listeners.
96 | *
97 | * This method must be called on the thread that is running the animation.
98 | */
99 | public void end() {
100 | }
101 |
102 | /**
103 | * This method tells the object to use appropriate information to extract
104 | * starting values for the animation. For example, a AnimatorSet object will pass
105 | * this call to its child objects to tell them to set up the values. A
106 | * ObjectAnimator object will use the information it has about its target object
107 | * and PropertyValuesHolder objects to get the start values for its properties.
108 | * A ValueAnimator object will ignore the request since it does not have enough
109 | * information (such as a target object) to gather these values.
110 | */
111 | public void setupStartValues() {
112 | }
113 |
114 | /**
115 | * This method tells the object to use appropriate information to extract
116 | * ending values for the animation. For example, a AnimatorSet object will pass
117 | * this call to its child objects to tell them to set up the values. A
118 | * ObjectAnimator object will use the information it has about its target object
119 | * and PropertyValuesHolder objects to get the start values for its properties.
120 | * A ValueAnimator object will ignore the request since it does not have enough
121 | * information (such as a target object) to gather these values.
122 | */
123 | public void setupEndValues() {
124 | }
125 |
126 | /**
127 | * Experimental feature
128 | */
129 | public SupportAnimator reverse() {
130 | if(isRunning()){
131 | return null;
132 | }
133 |
134 | RevealAnimator target = mTarget.get();
135 | if(target != null){
136 | return target.startReverseAnimation();
137 | }
138 |
139 | return null;
140 | }
141 |
142 | /**
143 | * An animation listener receives notifications from an animation.
144 | * Notifications indicate animation related events, such as the end or the
145 | * repetition of the animation.
146 | */
147 | public interface AnimatorListener {
148 | /**
149 | * Notifies the start of the animation.
150 | */
151 | void onAnimationStart();
152 |
153 | /**
154 | * Notifies the end of the animation. This callback is not invoked
155 | * for animations with repeat count set to INFINITE.
156 | */
157 | void onAnimationEnd();
158 |
159 | /**
160 | * Notifies the cancellation of the animation. This callback is not invoked
161 | * for animations with repeat count set to INFINITE.
162 | */
163 | void onAnimationCancel();
164 |
165 | /**
166 | * Notifies the repetition of the animation.
167 | */
168 | void onAnimationRepeat();
169 | }
170 |
171 | /**
172 | * Provides default implementation for AnimatorListener.
173 | */
174 | public static abstract class SimpleAnimatorListener implements AnimatorListener {
175 |
176 | @Override
177 | public void onAnimationStart() {
178 |
179 | }
180 |
181 | @Override
182 | public void onAnimationEnd() {
183 |
184 | }
185 |
186 | @Override
187 | public void onAnimationCancel() {
188 |
189 | }
190 |
191 | @Override
192 | public void onAnimationRepeat() {
193 |
194 | }
195 | }
196 |
197 | }
--------------------------------------------------------------------------------
/sample/src/main/java/org/cryse/widget/persistentsearch/sample/MenuItemSampleActivity.java:
--------------------------------------------------------------------------------
1 | package org.cryse.widget.persistentsearch.sample;
2 |
3 | import android.animation.Animator;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 | import android.speech.RecognizerIntent;
7 | import android.support.v7.app.ActionBar;
8 | import android.support.v7.app.AppCompatActivity;
9 | import android.support.v7.widget.DefaultItemAnimator;
10 | import android.support.v7.widget.LinearLayoutManager;
11 | import android.support.v7.widget.RecyclerView;
12 | import android.support.v7.widget.Toolbar;
13 | import android.util.Log;
14 | import android.view.Menu;
15 | import android.view.MenuInflater;
16 | import android.view.MenuItem;
17 | import android.view.View;
18 | import android.widget.Toast;
19 |
20 | import org.cryse.widget.persistentsearch.DefaultVoiceRecognizerDelegate;
21 | import org.cryse.widget.persistentsearch.PersistentSearchView;
22 | import org.cryse.widget.persistentsearch.PersistentSearchView.HomeButtonListener;
23 | import org.cryse.widget.persistentsearch.PersistentSearchView.SearchListener;
24 | import org.cryse.widget.persistentsearch.SearchItem;
25 | import org.cryse.widget.persistentsearch.VoiceRecognitionDelegate;
26 |
27 | import java.util.ArrayList;
28 | import java.util.List;
29 |
30 | public class MenuItemSampleActivity extends AppCompatActivity {
31 | private static final int VOICE_RECOGNITION_REQUEST_CODE = 1023;
32 | private PersistentSearchView mSearchView;
33 | private Toolbar mToolbar;
34 | private MenuItem mSearchMenuItem;
35 | private View mSearchTintView;
36 | private SearchResultAdapter mResultAdapter;
37 | private RecyclerView mRecyclerView;
38 |
39 | @Override
40 | public void onCreate(Bundle savedInstanceState) {
41 | super.onCreate(savedInstanceState);
42 | setContentView(R.layout.activity_menu_item_sample);
43 | mSearchView = (PersistentSearchView) findViewById(R.id.searchview);
44 | VoiceRecognitionDelegate delegate = new DefaultVoiceRecognizerDelegate(this, VOICE_RECOGNITION_REQUEST_CODE);
45 | if(delegate.isVoiceRecognitionAvailable()) {
46 | mSearchView.setVoiceRecognitionDelegate(delegate);
47 | }
48 | mToolbar = (Toolbar) findViewById(R.id.toolbar);
49 | mSearchTintView = findViewById(R.id.view_search_tint);
50 | this.setSupportActionBar(mToolbar);
51 | ActionBar actionBar = getSupportActionBar();
52 | if (actionBar != null) {
53 | actionBar.setDisplayShowHomeEnabled(true);
54 | actionBar.setDisplayHomeAsUpEnabled(true);
55 | }
56 | setUpSearchView();
57 | }
58 |
59 | @Override
60 | public boolean onCreateOptionsMenu(Menu menu) {
61 | MenuInflater inflater = getMenuInflater();
62 | inflater.inflate(R.menu.menu_searchview, menu);
63 | mSearchMenuItem = menu.findItem(R.id.action_search);
64 | return true;
65 | }
66 |
67 | @Override
68 | public boolean onOptionsItemSelected(MenuItem item) {
69 | switch (item.getItemId()) {
70 | case android.R.id.home:
71 | finish();
72 | return true;
73 | case R.id.action_search:
74 | if(mSearchMenuItem != null) {
75 | openSearch();
76 | return true;
77 | } else {
78 | return false;
79 | }
80 | }
81 | return super.onOptionsItemSelected(item);
82 | }
83 |
84 | public void openSearch() {
85 | View menuItemView = findViewById(R.id.action_search);
86 | mSearchView.setStartPositionFromMenuItem(menuItemView);
87 | mSearchView.openSearch();
88 | }
89 |
90 | public void setUpSearchView() {
91 | mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview_search_result);
92 | mRecyclerView.setItemAnimator(new DefaultItemAnimator());
93 | mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
94 | mResultAdapter = new SearchResultAdapter(new ArrayList());
95 | mRecyclerView.setAdapter(mResultAdapter);
96 | mSearchTintView.setOnClickListener(new View.OnClickListener() {
97 | @Override
98 | public void onClick(View v) {
99 | mSearchView.cancelEditing();
100 | }
101 | });
102 | mSearchView.setHomeButtonListener(new HomeButtonListener() {
103 |
104 | @Override
105 | public void onHomeButtonClick() {
106 | // Hamburger has been clicked
107 | Toast.makeText(MenuItemSampleActivity.this, "Menu click",
108 | Toast.LENGTH_LONG).show();
109 | }
110 |
111 | });
112 | mSearchView.setSuggestionBuilder(new SampleSuggestionsBuilder(this));
113 | mSearchView.setSearchListener(new SearchListener() {
114 |
115 | @Override
116 | public boolean onSuggestion(SearchItem searchItem) {
117 | Log.d("onSuggestion", searchItem.getTitle());
118 | return false;
119 | }
120 |
121 | @Override
122 | public void onSearchEditOpened() {
123 | //Use this to tint the screen
124 | mSearchTintView.setVisibility(View.VISIBLE);
125 | mSearchTintView
126 | .animate()
127 | .alpha(1.0f)
128 | .setDuration(300)
129 | .setListener(new SimpleAnimationListener())
130 | .start();
131 |
132 | }
133 |
134 | @Override
135 | public void onSearchEditClosed() {
136 | mSearchTintView
137 | .animate()
138 | .alpha(0.0f)
139 | .setDuration(300)
140 | .setListener(new SimpleAnimationListener() {
141 | @Override
142 | public void onAnimationEnd(Animator animation) {
143 | super.onAnimationEnd(animation);
144 | mSearchTintView.setVisibility(View.GONE);
145 | }
146 | })
147 | .start();
148 | }
149 |
150 | @Override
151 | public boolean onSearchEditBackPressed() {
152 | if(mSearchView.isEditing()) {
153 | mSearchView.cancelEditing();
154 | return true;
155 | }
156 | return false;
157 | }
158 |
159 | @Override
160 | public void onSearchExit() {
161 | mResultAdapter.clear();
162 | if (mRecyclerView.getVisibility() == View.VISIBLE) {
163 | mRecyclerView.setVisibility(View.GONE);
164 | }
165 | }
166 |
167 | @Override
168 | public void onSearchTermChanged(String term) {
169 |
170 | }
171 |
172 | @Override
173 | public void onSearch(String string) {
174 | Toast.makeText(MenuItemSampleActivity.this, string + " Searched", Toast.LENGTH_LONG).show();
175 | mRecyclerView.setVisibility(View.VISIBLE);
176 | fillResultToRecyclerView(string);
177 | }
178 |
179 | @Override
180 | public void onSearchCleared() {
181 |
182 | }
183 |
184 | });
185 |
186 | }
187 |
188 | private void fillResultToRecyclerView(String query) {
189 | List newResults = new ArrayList<>();
190 | for(int i =0; i< 10; i++) {
191 | SearchResult result = new SearchResult(query, query + Integer.toString(i), "");
192 | newResults.add(result);
193 | }
194 | mResultAdapter.replaceWith(newResults);
195 | }
196 |
197 | @Override
198 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
199 | if (requestCode == VOICE_RECOGNITION_REQUEST_CODE && resultCode == RESULT_OK) {
200 | ArrayList matches = data
201 | .getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
202 | mSearchView.populateEditText(matches);
203 | }
204 | super.onActivityResult(requestCode, resultCode, data);
205 | }
206 |
207 | @Override
208 | public void onBackPressed() {
209 | if(mSearchView.isSearching()) {
210 | mSearchView.closeSearch();
211 | } else if(mRecyclerView.getVisibility() == View.VISIBLE) {
212 | mResultAdapter.clear();
213 | mRecyclerView.setVisibility(View.GONE);
214 | } else {
215 | super.onBackPressed();
216 | }
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/sample/src/main/java/org/cryse/widget/persistentsearch/sample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package org.cryse.widget.persistentsearch.sample;
2 |
3 | import android.animation.Animator;
4 | import android.app.Activity;
5 | import android.content.Intent;
6 | import android.net.Uri;
7 | import android.os.Bundle;
8 | import android.speech.RecognizerIntent;
9 | import android.support.v7.widget.DefaultItemAnimator;
10 | import android.support.v7.widget.LinearLayoutManager;
11 | import android.support.v7.widget.RecyclerView;
12 | import android.util.Log;
13 | import android.view.View;
14 | import android.widget.Button;
15 | import android.widget.Toast;
16 |
17 | import org.cryse.widget.persistentsearch.DefaultVoiceRecognizerDelegate;
18 | import org.cryse.widget.persistentsearch.PersistentSearchView;
19 | import org.cryse.widget.persistentsearch.PersistentSearchView.HomeButtonListener;
20 | import org.cryse.widget.persistentsearch.PersistentSearchView.SearchListener;
21 | import org.cryse.widget.persistentsearch.SearchItem;
22 | import org.cryse.widget.persistentsearch.VoiceRecognitionDelegate;
23 |
24 | import java.util.ArrayList;
25 | import java.util.List;
26 |
27 | public class MainActivity extends Activity {
28 | private static final int VOICE_RECOGNITION_REQUEST_CODE = 1023;
29 | private PersistentSearchView mSearchView;
30 | private Button mMenuItemSampleButton;
31 | private Button mSearchSampleButton;
32 | private Button mGithubRepoButton;
33 | private View mSearchTintView;
34 | private SearchResultAdapter mResultAdapter;
35 | private RecyclerView mRecyclerView;
36 | @Override
37 | protected void onCreate(Bundle savedInstanceState) {
38 | super.onCreate(savedInstanceState);
39 | setContentView(R.layout.activity_main);
40 | mSearchView = (PersistentSearchView) findViewById(R.id.searchview);
41 | mSearchTintView = findViewById(R.id.view_search_tint);
42 | mMenuItemSampleButton = (Button) findViewById(R.id.button_reveal_sample);
43 | mSearchSampleButton = (Button) findViewById(R.id.button_search_sample);
44 | mGithubRepoButton = (Button) findViewById(R.id.button_github_repo);
45 | mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview_search_result);
46 | mRecyclerView.setItemAnimator(new DefaultItemAnimator());
47 | mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
48 | mResultAdapter = new SearchResultAdapter(new ArrayList());
49 | mRecyclerView.setAdapter(mResultAdapter);
50 | mMenuItemSampleButton.setOnClickListener(new View.OnClickListener() {
51 | @Override
52 | public void onClick(View v) {
53 | startActivity(new Intent(MainActivity.this, MenuItemSampleActivity.class));
54 | }
55 | });
56 | mSearchSampleButton.setOnClickListener(new View.OnClickListener() {
57 | @Override
58 | public void onClick(View v) {
59 | startActivity(new Intent(MainActivity.this, SearchActivity.class));
60 | }
61 | });
62 | mGithubRepoButton.setOnClickListener(new View.OnClickListener() {
63 | @Override
64 | public void onClick(View v) {
65 | Intent i = new Intent(Intent.ACTION_VIEW);
66 | i.setData(Uri.parse(getString(R.string.url_github_repo)));
67 | startActivity(i);
68 | }
69 | });
70 | VoiceRecognitionDelegate delegate = new DefaultVoiceRecognizerDelegate(this, VOICE_RECOGNITION_REQUEST_CODE);
71 | if(delegate.isVoiceRecognitionAvailable()) {
72 | mSearchView.setVoiceRecognitionDelegate(delegate);
73 | }
74 | mSearchView.setHomeButtonListener(new HomeButtonListener() {
75 |
76 | @Override
77 | public void onHomeButtonClick() {
78 | //Hamburger has been clicked
79 | Toast.makeText(MainActivity.this, "Menu click", Toast.LENGTH_LONG).show();
80 | }
81 |
82 | });
83 | mSearchTintView.setOnClickListener(new View.OnClickListener() {
84 | @Override
85 | public void onClick(View v) {
86 | mSearchView.cancelEditing();
87 | }
88 | });
89 | mSearchView.setSuggestionBuilder(new SampleSuggestionsBuilder(this));
90 | mSearchView.setSearchListener(new SearchListener() {
91 |
92 | @Override
93 | public void onSearchEditOpened() {
94 | //Use this to tint the screen
95 | mSearchTintView.setVisibility(View.VISIBLE);
96 | mSearchTintView
97 | .animate()
98 | .alpha(1.0f)
99 | .setDuration(300)
100 | .setListener(new SimpleAnimationListener())
101 | .start();
102 | }
103 |
104 | @Override
105 | public void onSearchEditClosed() {
106 | mSearchTintView
107 | .animate()
108 | .alpha(0.0f)
109 | .setDuration(300)
110 | .setListener(new SimpleAnimationListener() {
111 | @Override
112 | public void onAnimationEnd(Animator animation) {
113 | super.onAnimationEnd(animation);
114 | mSearchTintView.setVisibility(View.GONE);
115 | }
116 | })
117 | .start();
118 | }
119 |
120 | @Override
121 | public boolean onSearchEditBackPressed() {
122 | return false;
123 | }
124 |
125 | @Override
126 | public void onSearchExit() {
127 | mResultAdapter.clear();
128 | if(mRecyclerView.getVisibility() == View.VISIBLE) {
129 | mRecyclerView.setVisibility(View.GONE);
130 | }
131 | }
132 |
133 | @Override
134 | public void onSearchTermChanged(String term) {
135 |
136 | }
137 |
138 | @Override
139 | public void onSearch(String string) {
140 | Toast.makeText(MainActivity.this, string +" Searched", Toast.LENGTH_LONG).show();
141 | mRecyclerView.setVisibility(View.VISIBLE);
142 | fillResultToRecyclerView(string);
143 | }
144 |
145 | @Override
146 | public boolean onSuggestion(SearchItem searchItem) {
147 | Log.d("onSuggestion", searchItem.getTitle());
148 | return false;
149 | }
150 |
151 | @Override
152 | public void onSearchCleared() {
153 | //Called when the clear button is clicked
154 | }
155 |
156 | });
157 | }
158 |
159 | @Override
160 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
161 | if (requestCode == VOICE_RECOGNITION_REQUEST_CODE && resultCode == RESULT_OK) {
162 | ArrayList matches = data
163 | .getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
164 | mSearchView.populateEditText(matches);
165 | }
166 | super.onActivityResult(requestCode, resultCode, data);
167 | }
168 |
169 | private void fillResultToRecyclerView(String query) {
170 | List newResults = new ArrayList<>();
171 | for(int i =0; i< 10; i++) {
172 | SearchResult result = new SearchResult(query, query + Integer.toString(i), "");
173 | newResults.add(result);
174 | }
175 | mResultAdapter.replaceWith(newResults);
176 | }
177 |
178 | @Override
179 | public void onBackPressed() {
180 | if(mSearchView.isSearching()) {
181 | mSearchView.closeSearch();
182 | } else if(mRecyclerView.getVisibility() == View.VISIBLE) {
183 | mResultAdapter.clear();
184 | mRecyclerView.setVisibility(View.GONE);
185 | } else {
186 | super.onBackPressed();
187 | }
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/persistentsearchview/src/main/java/org/cryse/widget/persistentsearch/PersistentSearchView.java:
--------------------------------------------------------------------------------
1 | package org.cryse.widget.persistentsearch;
2 |
3 | import android.animation.LayoutTransition;
4 | import android.content.Context;
5 | import android.content.res.Resources;
6 | import android.content.res.TypedArray;
7 | import android.graphics.Color;
8 | import android.graphics.drawable.Drawable;
9 | import android.inputmethodservice.KeyboardView;
10 | import android.os.Build;
11 | import android.os.Parcel;
12 | import android.os.Parcelable;
13 | import android.support.v4.content.res.ResourcesCompat;
14 | import android.support.v4.view.ViewCompat;
15 | import android.support.v7.widget.CardView;
16 | import android.text.Editable;
17 | import android.text.Layout;
18 | import android.text.TextUtils;
19 | import android.text.TextWatcher;
20 | import android.util.AttributeSet;
21 | import android.util.DisplayMetrics;
22 | import android.util.SparseArray;
23 | import android.util.TypedValue;
24 | import android.view.KeyEvent;
25 | import android.view.LayoutInflater;
26 | import android.view.MotionEvent;
27 | import android.view.View;
28 | import android.view.ViewTreeObserver;
29 | import android.view.animation.AccelerateDecelerateInterpolator;
30 | import android.view.inputmethod.EditorInfo;
31 | import android.view.inputmethod.InputMethodManager;
32 | import android.widget.AdapterView;
33 | import android.widget.EditText;
34 | import android.widget.FrameLayout;
35 | import android.widget.ImageView;
36 | import android.widget.ListView;
37 | import android.widget.RelativeLayout;
38 | import android.widget.TextView;
39 |
40 | import java.util.ArrayList;
41 | import java.util.Collection;
42 |
43 | import io.codetail.animation.SupportAnimator;
44 | import io.codetail.animation.ViewAnimationUtils;
45 |
46 | @SuppressWarnings("unused")
47 | public class PersistentSearchView extends RevealViewGroup {
48 | public static final int VOICE_RECOGNITION_CODE = 8185102;
49 | final static double COS_45 = Math.cos(Math.toRadians(45));
50 | private static final int[] RES_IDS_ACTION_BAR_SIZE = { R.attr.actionBarSize };
51 | private static final int DURATION_REVEAL_OPEN = 400;
52 | private static final int DURATION_REVEAL_CLOSE = 300;
53 | private static final int DURATION_HOME_BUTTON = 300;
54 | private static final int DURATION_LAYOUT_TRANSITION = 100;
55 | private HomeButton.IconState mHomeButtonCloseIconState;
56 | private HomeButton.IconState mHomeButtonOpenIconState;
57 | private HomeButton.IconState mHomeButtonSearchIconState;
58 | private SearchViewState mCurrentState;
59 | private SearchViewState mLastState;
60 | private DisplayMode mDisplayMode;
61 | private int mHomeButtonMode;
62 | private int mCardVerticalPadding;
63 | private int mCardHorizontalPadding;
64 | private int mCardHeight;
65 | private int mCustomToolbarHeight;
66 | private int mSearchCardElevation;
67 | private int mFromX, mFromY, mDesireRevealWidth;
68 | // Views
69 | private LogoView mLogoView;
70 | private CardView mSearchCardView;
71 | private HomeButton mHomeButton;
72 | private EditText mSearchEditText;
73 | private ListView mSuggestionListView;
74 | private ImageView mMicButton;
75 | private SearchListener mSearchListener;
76 | private HomeButtonListener mHomeButtonListener;
77 | private FrameLayout mRootLayout;
78 | private VoiceRecognitionDelegate mVoiceRecognitionDelegate;
79 | private boolean mAvoidTriggerTextWatcher;
80 | private boolean mIsMic;
81 | private int mSearchTextColor;
82 | private int mArrorButtonColor;
83 | private Drawable mLogoDrawable;
84 | private String mStringLogoDrawable;
85 | private int mSearchEditTextColor;
86 | private String mSearchEditTextHint;
87 | private int mSearchEditTextHintColor;
88 | private SearchSuggestionsBuilder mSuggestionBuilder;
89 | private SearchItemAdapter mSearchItemAdapter;
90 | private ArrayList mSearchSuggestions;
91 | private KeyboardView mCustomKeyboardView;
92 | private boolean showCustomKeyboard;
93 |
94 | public PersistentSearchView(Context context) {
95 | super(context);
96 | init(null);
97 | }
98 | public PersistentSearchView(Context context, AttributeSet attrs) {
99 | super(context, attrs);
100 | init(attrs);
101 | }
102 |
103 | public PersistentSearchView(Context context, AttributeSet attrs, int defStyle) {
104 | super(context, attrs, defStyle);
105 | init(attrs);
106 | }
107 |
108 | public void setCustomKeyboardView(KeyboardView customKeyboardView)
109 | {
110 | mCustomKeyboardView = customKeyboardView;
111 | }
112 |
113 | public void enableCustomKeyboardView(boolean enable)
114 | {
115 | showCustomKeyboard = enable;
116 | }
117 |
118 | static float calculateVerticalPadding(CardView cardView) {
119 | float maxShadowSize = cardView.getMaxCardElevation();
120 | float cornerRadius = cardView.getRadius();
121 | boolean addPaddingForCorners = cardView.getPreventCornerOverlap();
122 |
123 | if (addPaddingForCorners) {
124 | return (float) (maxShadowSize * 1.5f + (1 - COS_45) * cornerRadius);
125 | } else {
126 | return maxShadowSize * 1.5f;
127 | }
128 | }
129 |
130 | static float calculateHorizontalPadding(CardView cardView) {
131 | float maxShadowSize = cardView.getMaxCardElevation();
132 | float cornerRadius = cardView.getRadius();
133 | boolean addPaddingForCorners = cardView.getPreventCornerOverlap();
134 | if (addPaddingForCorners) {
135 | return (float) (maxShadowSize + (1 - COS_45) * cornerRadius);
136 | } else {
137 | return maxShadowSize;
138 | }
139 | }
140 |
141 | /** Calculates the Toolbar height in pixels. */
142 | static int calculateToolbarSize(Context context) {
143 | if (context == null) {
144 | return 0;
145 | }
146 |
147 | Resources.Theme curTheme = context.getTheme();
148 | if (curTheme == null) {
149 | return 0;
150 | }
151 |
152 | TypedArray att = curTheme.obtainStyledAttributes(RES_IDS_ACTION_BAR_SIZE);
153 | if (att == null) {
154 | return 0;
155 | }
156 |
157 | float size = att.getDimension(0, 0);
158 | att.recycle();
159 | return (int)size;
160 | }
161 |
162 | private void init(AttributeSet attrs) {
163 | setSaveEnabled(true);
164 | LayoutInflater.from(getContext()).inflate(R.layout.layout_searchview, this, true);
165 | if (attrs != null) {
166 | TypedArray attrsValue = getContext().obtainStyledAttributes(attrs,
167 | R.styleable.PersistentSearchView);
168 | mDisplayMode = DisplayMode.fromInt(attrsValue.getInt(R.styleable.PersistentSearchView_persistentSV_displayMode, DisplayMode.MENUITEM.toInt()));
169 | mSearchCardElevation = attrsValue.getDimensionPixelSize(R.styleable.PersistentSearchView_persistentSV_searchCardElevation, -1);
170 | mSearchTextColor = attrsValue.getColor(R.styleable.PersistentSearchView_persistentSV_searchTextColor, Color.BLACK);
171 | mLogoDrawable = attrsValue.getDrawable(R.styleable.PersistentSearchView_persistentSV_logoDrawable);
172 | mStringLogoDrawable = attrsValue.getString(R.styleable.PersistentSearchView_persistentSV_logoString);
173 | mSearchEditTextColor = attrsValue.getColor(R.styleable.PersistentSearchView_persistentSV_editTextColor, Color.BLACK);
174 | mSearchEditTextHint = attrsValue.getString(R.styleable.PersistentSearchView_persistentSV_editHintText);
175 | mSearchEditTextHintColor = attrsValue.getColor(R.styleable.PersistentSearchView_persistentSV_editHintTextColor, Color.BLACK);
176 | mArrorButtonColor = attrsValue.getColor(R.styleable.PersistentSearchView_persistentSV_homeButtonColor, Color.BLACK);
177 | mCustomToolbarHeight = attrsValue.getDimensionPixelSize(R.styleable.PersistentSearchView_persistentSV_customToolbarHeight, calculateToolbarSize(getContext()));
178 | mHomeButtonMode = attrsValue.getInt(R.styleable.PersistentSearchView_persistentSV_homeButtonMode, 0);
179 | attrsValue.recycle();
180 | }
181 |
182 | if (mSearchCardElevation < 0) {
183 | mSearchCardElevation = getContext().getResources().getDimensionPixelSize(R.dimen.search_card_default_card_elevation);
184 | }
185 |
186 | mCardHeight = getResources().getDimensionPixelSize(R.dimen.search_card_height);
187 | mCardVerticalPadding = (mCustomToolbarHeight - mCardHeight) / 2;
188 |
189 | switch (mDisplayMode) {
190 | case MENUITEM:
191 | default:
192 | mCardHorizontalPadding = getResources().getDimensionPixelSize(R.dimen.search_card_visible_padding_menu_item_mode);
193 | if(mCardVerticalPadding > mCardHorizontalPadding)
194 | mCardHorizontalPadding = mCardVerticalPadding;
195 | mHomeButtonCloseIconState = HomeButton.IconState.ARROW;
196 | mHomeButtonOpenIconState = HomeButton.IconState.ARROW;
197 | setCurrentState(SearchViewState.NORMAL);
198 | break;
199 | case TOOLBAR:
200 | if(mHomeButtonMode == 0) { // Arrow Mode
201 | mHomeButtonCloseIconState = HomeButton.IconState.ARROW;
202 | mHomeButtonOpenIconState = HomeButton.IconState.ARROW;
203 | } else { // Burger Mode
204 | mHomeButtonCloseIconState = HomeButton.IconState.BURGER;
205 | mHomeButtonOpenIconState = HomeButton.IconState.ARROW;
206 | }
207 | mCardHorizontalPadding = getResources().getDimensionPixelSize(R.dimen.search_card_visible_padding_toolbar_mode);
208 | setCurrentState(SearchViewState.NORMAL);
209 | break;
210 | }
211 | mHomeButtonSearchIconState = HomeButton.IconState.ARROW;
212 |
213 | bindViews();
214 | setValuesToViews();
215 |
216 | this.mIsMic = true;
217 | mSearchSuggestions = new ArrayList<>();
218 | mSearchItemAdapter = new SearchItemAdapter(getContext(), mSearchSuggestions);
219 | mSuggestionListView.setAdapter(mSearchItemAdapter);
220 |
221 | setUpLayoutTransition();
222 | setUpListeners();
223 | }
224 |
225 | public ArrayList getSearchSuggestions() {
226 | return mSearchSuggestions;
227 | }
228 |
229 | private void bindViews() {
230 | this.mSearchCardView = (CardView) findViewById(R.id.cardview_search);
231 | this.mHomeButton = (HomeButton) findViewById(R.id.button_home);
232 | this.mLogoView = (LogoView) findViewById(R.id.logoview);
233 | this.mSearchEditText = (EditText) findViewById(R.id.edittext_search);
234 | this.mSuggestionListView = (ListView) findViewById(R.id.listview_suggestions);
235 | this.mMicButton = (ImageView) findViewById(R.id.button_mic);
236 | }
237 |
238 | private void setValuesToViews() {
239 | this.mSearchCardView.setCardElevation(mSearchCardElevation);
240 | this.mSearchCardView.setMaxCardElevation(mSearchCardElevation);
241 | this.mHomeButton.setArrowDrawableColor(mArrorButtonColor);
242 | this.mHomeButton.setState(mHomeButtonCloseIconState);
243 | this.mHomeButton.setAnimationDuration(DURATION_HOME_BUTTON);
244 | this.mSearchEditText.setTextColor(mSearchEditTextColor);
245 | this.mSearchEditText.setHint(mSearchEditTextHint);
246 | this.mSearchEditText.setHintTextColor(mSearchEditTextHintColor);
247 | if (mLogoDrawable != null) {
248 | this.mLogoView.setLogo(mLogoDrawable);
249 | }
250 | else if (mStringLogoDrawable != null)
251 | {
252 | this.mLogoView.setLogo(mStringLogoDrawable);
253 | }
254 | this.mLogoView.setTextColor(mSearchTextColor);
255 | }
256 |
257 | private void setUpListeners() {
258 | mHomeButton.setOnClickListener(new OnClickListener() {
259 | @Override
260 | public void onClick(View v) {
261 | if (mCurrentState == SearchViewState.EDITING) {
262 | cancelEditing();
263 | } else if (mCurrentState == SearchViewState.SEARCH) {
264 | fromSearchToNormal();
265 | } else {
266 | if (mHomeButtonListener != null)
267 | mHomeButtonListener.onHomeButtonClick();
268 | }
269 | }
270 |
271 | });
272 | mLogoView.setOnClickListener(new OnClickListener() {
273 |
274 | @Override
275 | public void onClick(View v) {
276 | dispatchStateChange(SearchViewState.EDITING); // This would call when state is wrong.
277 | }
278 |
279 | });
280 | mSearchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
281 | public boolean onEditorAction(TextView v, int actionId,
282 | KeyEvent event) {
283 | if (actionId == EditorInfo.IME_ACTION_SEARCH) {
284 | clearSuggestions();
285 | fromEditingToSearch(true, false);
286 | return true;
287 | }
288 | return false;
289 | }
290 | });
291 | mSearchEditText.setOnKeyListener(new OnKeyListener() {
292 | public boolean onKey(View v, int keyCode, KeyEvent event) {
293 | if (keyCode == KeyEvent.KEYCODE_ENTER) {
294 | clearSuggestions();
295 | fromEditingToSearch(true, false);
296 | return true;
297 | } else if (keyCode == KeyEvent.KEYCODE_BACK) {
298 | return mSearchListener != null && mSearchListener.onSearchEditBackPressed();
299 | }
300 | return false;
301 | }
302 | });
303 | micStateChanged();
304 | mMicButton.setOnClickListener(new OnClickListener() {
305 | @Override
306 | public void onClick(View v) {
307 | micClick();
308 | }
309 | });
310 | mSearchEditText.addTextChangedListener(new TextWatcher() {
311 |
312 | @Override
313 | public void afterTextChanged(Editable s) {
314 | if (!mAvoidTriggerTextWatcher) {
315 | if (s.length() > 0) {
316 | showClearButton();
317 | buildSearchSuggestions(getSearchText());
318 | } else {
319 | showMicButton();
320 | buildEmptySearchSuggestions();
321 | }
322 | }
323 | if (mSearchListener != null)
324 | mSearchListener.onSearchTermChanged(s.toString());
325 | }
326 |
327 | @Override
328 | public void beforeTextChanged(CharSequence s, int start, int count,
329 | int after) {
330 | }
331 |
332 | @Override
333 | public void onTextChanged(CharSequence s, int start, int before,
334 | int count) {
335 |
336 | }
337 |
338 | });
339 |
340 | }
341 |
342 | private void setUpLayoutTransition() {
343 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
344 | RelativeLayout searchRoot = (RelativeLayout) findViewById(R.id.search_root);
345 | LayoutTransition layoutTransition = new LayoutTransition();
346 | layoutTransition.setDuration(DURATION_LAYOUT_TRANSITION);
347 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
348 | // layoutTransition.enableTransitionType(LayoutTransition.CHANGING);
349 | layoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
350 | layoutTransition.setStartDelay(LayoutTransition.CHANGING, 0);
351 | }
352 | layoutTransition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
353 | mSearchCardView.setLayoutTransition(layoutTransition);
354 | }
355 | }
356 |
357 | @Override
358 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
359 | final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
360 | final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
361 |
362 | int totalHeight = 0;
363 | int searchCardWidth;
364 | final int childCount = getChildCount();
365 | for (int i = 0; i < childCount; ++i) {
366 | final View child = getChildAt(i);
367 | if (child.getVisibility() != GONE) {
368 | if (i == 0 && child instanceof CardView) {
369 | CardView searchCard = (CardView) child;
370 | int horizontalPadding = (int) Math.ceil(calculateHorizontalPadding(searchCard));
371 | int verticalPadding = (int) Math.ceil(calculateVerticalPadding(searchCard));
372 | // searchCardWidth = widthSize - 2 * mCardVisiblePadding + horizontalPadding * 2;
373 | int searchCardLeft = mCardHorizontalPadding - horizontalPadding;
374 | // searchCardTop = mCardVisiblePadding - verticalPadding;
375 | searchCardWidth = widthSize - searchCardLeft * 2;
376 | int cardWidthSpec = MeasureSpec.makeMeasureSpec(searchCardWidth, MeasureSpec.EXACTLY);
377 | // int cardHeightSpec = MeasureSpec.makeMeasureSpec(searchCardHeight, MeasureSpec.EXACTLY);
378 | measureChild(child, cardWidthSpec, heightMeasureSpec);
379 | int childMeasuredHeight = child.getMeasuredHeight();
380 | int childMeasuredWidth = child.getMeasuredWidth();
381 | int childHeight = childMeasuredHeight - verticalPadding * 2;
382 | totalHeight = totalHeight + childHeight + mCardVerticalPadding * 2;
383 | }
384 | }
385 | }
386 | if(totalHeight < mCustomToolbarHeight)
387 | totalHeight = mCustomToolbarHeight;
388 | setMeasuredDimension(widthSize, totalHeight);
389 | }
390 |
391 | @Override
392 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
393 | final int count = getChildCount();
394 | int searchViewWidth = r - l;
395 | int searchViewHeight = b - t;
396 | int searchCardLeft;
397 | int searchCardTop;
398 | int searchCardRight;
399 | int searchCardBottom;
400 | int searchCardWidth;
401 | int searchCardHeight;
402 | for (int i = 0; i < count; i++) {
403 | View child = getChildAt(i);
404 | if (i == 0 && child instanceof CardView) {
405 | CardView searchCard = (CardView) child;
406 | int horizontalPadding = (int) Math.ceil(calculateHorizontalPadding(searchCard));
407 | int verticalPadding = (int) Math.ceil(calculateVerticalPadding(searchCard));
408 | searchCardLeft = mCardHorizontalPadding - horizontalPadding;
409 | searchCardTop = mCardVerticalPadding - verticalPadding;
410 | searchCardWidth = searchViewWidth - searchCardLeft * 2;
411 | searchCardHeight = child.getMeasuredHeight();
412 | searchCardRight = searchCardLeft + searchCardWidth;
413 | searchCardBottom = searchCardTop + searchCardHeight;
414 | child.layout(searchCardLeft, searchCardTop, searchCardRight, searchCardBottom);
415 | }
416 | }
417 | }
418 |
419 | private void revealFromMenuItem() {
420 | setVisibility(View.VISIBLE);
421 | revealFrom(mFromX, mFromY, mDesireRevealWidth);
422 | }
423 |
424 | private void hideCircularlyToMenuItem() {
425 | if (mFromX == 0 || mFromY == 0) {
426 | mFromX = getRight();
427 | mFromY = getTop();
428 | }
429 | hideCircularly(mFromX, mFromY);
430 | }
431 |
432 | /***
433 | * Hide the PersistentSearchView using the circle animation. Can be called regardless of result list length
434 | */
435 | private void hideCircularly(int x, int y) {
436 |
437 | Resources r = getResources();
438 | float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96,
439 | r.getDisplayMetrics());
440 | int finalRadius = (int) Math.max(this.getMeasuredWidth() * 1.5, px);
441 |
442 | SupportAnimator animator = ViewAnimationUtils.createCircularReveal(
443 | mSearchCardView, x, y, 0, finalRadius);
444 | animator = animator.reverse();
445 | animator.setInterpolator(new AccelerateDecelerateInterpolator());
446 | animator.setDuration(DURATION_REVEAL_CLOSE);
447 | animator.start();
448 | animator.addListener(new SupportAnimator.AnimatorListener() {
449 |
450 | @Override
451 | public void onAnimationStart() {
452 |
453 | }
454 |
455 | @Override
456 | public void onAnimationEnd() {
457 | setVisibility(View.GONE);
458 | closeSearchInternal();
459 | // closeSearch();
460 | }
461 |
462 | @Override
463 | public void onAnimationCancel() {
464 |
465 | }
466 |
467 | @Override
468 | public void onAnimationRepeat() {
469 |
470 | }
471 |
472 | });
473 | }
474 |
475 | private void hideCircularly() {
476 | hideCircularly(getLeft() + getRight(), getTop());
477 | }
478 |
479 | public boolean getSearchOpen() {
480 | return getVisibility() == VISIBLE && (mCurrentState == SearchViewState.SEARCH || mCurrentState == SearchViewState.EDITING);
481 | }
482 |
483 | /***
484 | * Hide the search suggestions manually
485 | */
486 | public void hideSuggestions() {
487 | this.mSearchEditText.setVisibility(View.GONE);
488 | this.mSuggestionListView.setVisibility(View.GONE);
489 | }
490 |
491 | private boolean isMicEnabled() {
492 | return mVoiceRecognitionDelegate != null;
493 | }
494 |
495 | private void micStateChanged() {
496 | mMicButton.setVisibility((!mIsMic || isMicEnabled()) ? VISIBLE : INVISIBLE);
497 | }
498 |
499 | private void micStateChanged(boolean isMic) {
500 | this.mIsMic = isMic;
501 | micStateChanged();
502 | }
503 |
504 | private void showMicButton() {
505 | micStateChanged(true);
506 | mMicButton.setImageDrawable(
507 | ResourcesCompat.getDrawable(getResources(), R.drawable.ic_action_mic_black, null));
508 | }
509 |
510 | private void showClearButton() {
511 | micStateChanged(false);
512 | mMicButton.setImageDrawable(
513 | ResourcesCompat.getDrawable(getResources(), R.drawable.ic_action_clear_black, null));
514 | }
515 |
516 | /***
517 | * Mandatory method for the onClick event
518 | */
519 | public void micClick() {
520 | if (!mIsMic) {
521 | setSearchString("", false);
522 | } else {
523 | if (mVoiceRecognitionDelegate != null)
524 | mVoiceRecognitionDelegate.onStartVoiceRecognition();
525 | }
526 | }
527 |
528 | /***
529 | * Populate the PersistentSearchView with words, in an ArrayList. Used by the voice input
530 | *
531 | * @param matches Matches
532 | */
533 | public void populateEditText(ArrayList matches) {
534 | String text = matches.get(0).trim();
535 | populateEditText(text);
536 | }
537 |
538 | /***
539 | * Populate the PersistentSearchView with search query
540 | *
541 | * @param query Matches
542 | */
543 | public void populateEditText(String query) {
544 | String text = query.trim();
545 | setSearchString(text, true);
546 | dispatchStateChange(SearchViewState.SEARCH);
547 | }
548 |
549 | /***
550 | * Set whether the menu button should be shown. Particularly useful for apps that adapt to screen sizes
551 | *
552 | * @param visibility Whether to show
553 | */
554 |
555 | public void setHomeButtonVisibility(int visibility) {
556 | this.mHomeButton.setVisibility(visibility);
557 | }
558 |
559 | /***
560 | * Set the menu listener
561 | *
562 | * @param homeButtonListener MenuListener
563 | */
564 | public void setHomeButtonListener(HomeButtonListener homeButtonListener) {
565 | this.mHomeButtonListener = homeButtonListener;
566 | }
567 |
568 | /***
569 | * Set the search listener
570 | *
571 | * @param listener SearchListener
572 | */
573 | public void setSearchListener(SearchListener listener) {
574 | this.mSearchListener = listener;
575 | }
576 |
577 | /***
578 | * Set the text color of the logo
579 | *
580 | * @param color logo text color
581 | */
582 | public void setLogoTextColor(int color) {
583 | mLogoView.setTextColor(color);
584 | }
585 |
586 | /***
587 | * Get the PersistentSearchView's current text
588 | *
589 | * @return Text
590 | */
591 | public String getSearchText() {
592 | return mSearchEditText.getText().toString();
593 | }
594 |
595 | public void clearSuggestions() {
596 | mSearchItemAdapter.clear();
597 | }
598 |
599 | /***
600 | * Set the PersistentSearchView's current text manually
601 | *
602 | * @param text Text
603 | * @param avoidTriggerTextWatcher avoid trigger TextWatcher(TextChangedListener)
604 | */
605 | public void setSearchString(String text, boolean avoidTriggerTextWatcher) {
606 | if (avoidTriggerTextWatcher)
607 | mAvoidTriggerTextWatcher = true;
608 | mSearchEditText.setText("");
609 | mSearchEditText.append(text);
610 | mAvoidTriggerTextWatcher = false;
611 | }
612 |
613 | private void buildEmptySearchSuggestions() {
614 | if (mSuggestionBuilder != null) {
615 | mSearchSuggestions.clear();
616 | Collection suggestions = mSuggestionBuilder.buildEmptySearchSuggestion(10);
617 | if (suggestions != null && suggestions.size() > 0) {
618 | mSearchSuggestions.addAll(suggestions);
619 | }
620 | mSearchItemAdapter.notifyDataSetChanged();
621 | }
622 | }
623 |
624 | private void buildSearchSuggestions(String query) {
625 | if (mSuggestionBuilder != null) {
626 | mSearchSuggestions.clear();
627 | Collection suggestions = mSuggestionBuilder.buildSearchSuggestion(10, query);
628 | if (suggestions != null && suggestions.size() > 0) {
629 | mSearchSuggestions.addAll(suggestions);
630 | }
631 | mSearchItemAdapter.notifyDataSetChanged();
632 | }
633 | }
634 |
635 | private void revealFrom(float x, float y, int desireRevealWidth) {
636 | Resources r = getResources();
637 | float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96,
638 | r.getDisplayMetrics());
639 | if(desireRevealWidth <= 0)
640 | desireRevealWidth = getMeasuredWidth();
641 | if(desireRevealWidth <= 0) {
642 | DisplayMetrics metrics = getResources().getDisplayMetrics();
643 | desireRevealWidth = metrics.widthPixels;
644 | }
645 | if(x <= 0 )
646 | x = desireRevealWidth - mCardHeight / 2;
647 | if(y <= 0)
648 | y = mCardHeight / 2;
649 |
650 | int measuredHeight = getMeasuredWidth();
651 | int finalRadius = (int) Math.max(Math.max(measuredHeight, px), desireRevealWidth);
652 |
653 | SupportAnimator animator = ViewAnimationUtils.createCircularReveal(
654 | mSearchCardView, (int) x, (int) y, 0, finalRadius);
655 | animator.setInterpolator(new AccelerateDecelerateInterpolator());
656 | animator.setDuration(DURATION_REVEAL_OPEN);
657 | animator.addListener(new SupportAnimator.AnimatorListener() {
658 |
659 | @Override
660 | public void onAnimationCancel() {
661 |
662 | }
663 |
664 | @Override
665 | public void onAnimationEnd() {
666 | // show search view here
667 | openSearchInternal(true);
668 | }
669 |
670 | @Override
671 | public void onAnimationRepeat() {
672 |
673 | }
674 |
675 | @Override
676 | public void onAnimationStart() {
677 |
678 | }
679 |
680 | });
681 | animator.start();
682 | }
683 |
684 | private void search() {
685 | String searchTerm = getSearchText();
686 | if (!TextUtils.isEmpty(searchTerm)) {
687 | setLogoTextInt(searchTerm);
688 | if (mSearchListener != null)
689 | mSearchListener.onSearch(searchTerm);
690 | }
691 | }
692 |
693 | private void openSearchInternal(Boolean openKeyboard) {
694 | this.mLogoView.setVisibility(View.GONE);
695 | this.mSearchEditText.setVisibility(View.VISIBLE);
696 | mSearchEditText.requestFocus();
697 | this.mSuggestionListView.setVisibility(View.VISIBLE);
698 | mSuggestionListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
699 |
700 | @Override
701 | public void onItemClick(AdapterView> arg0, View arg1, int arg2, long arg3) {
702 | hideKeyboard();
703 | SearchItem result = mSearchSuggestions.get(arg2);
704 | if (mSearchListener != null) {
705 | if (mSearchListener.onSuggestion(result)) {
706 | setSearchString(result.getValue(), true);
707 | fromEditingToSearch(true, false);
708 | }
709 | } else {
710 | setSearchString(result.getValue(), true);
711 | fromEditingToSearch(true, false);
712 | }
713 | }
714 |
715 | });
716 | String currentSearchText = getSearchText();
717 | if (currentSearchText.length() > 0) {
718 | buildSearchSuggestions(currentSearchText);
719 | } else {
720 | buildEmptySearchSuggestions();
721 | }
722 |
723 | if (mSearchListener != null)
724 | mSearchListener.onSearchEditOpened();
725 | if (getSearchText().length() > 0) {
726 | showClearButton();
727 | }
728 | if (openKeyboard) {
729 | if(showCustomKeyboard && mCustomKeyboardView != null) { // Show custom keyboard
730 | mCustomKeyboardView.setVisibility(View.VISIBLE);
731 | mCustomKeyboardView.setEnabled(true);
732 |
733 | // Enable cursor, but still prevent default keyboard from showing up
734 | OnTouchListener otl = new OnTouchListener() {
735 | @Override
736 | public boolean onTouch(View v, MotionEvent event) {
737 | switch (event.getAction()) {
738 | case MotionEvent.ACTION_DOWN:
739 | mCustomKeyboardView.setVisibility(View.VISIBLE);
740 | mCustomKeyboardView.setEnabled(true);
741 | Layout layout = ((EditText) v).getLayout();
742 | float x = event.getX() + mSearchEditText.getScrollX();
743 | int offset = layout.getOffsetForHorizontal(0, x);
744 | if (offset > 0)
745 | if (x > layout.getLineMax(0))
746 | mSearchEditText.setSelection(offset); // Touch was at the end of the text
747 | else
748 | mSearchEditText.setSelection(offset - 1);
749 | break;
750 | case MotionEvent.ACTION_MOVE:
751 | layout = ((EditText) v).getLayout();
752 | x = event.getX() + mSearchEditText.getScrollX();
753 | offset = layout.getOffsetForHorizontal(0, x);
754 | if (offset > 0)
755 | if (x > layout.getLineMax(0))
756 | mSearchEditText.setSelection(offset); // Touch point was at the end of the text
757 | else
758 | mSearchEditText.setSelection(offset - 1);
759 | break;
760 | }
761 | return true;
762 | }
763 | };
764 | mSearchEditText.setOnTouchListener(otl);
765 | } else { // Show default keyboard
766 | mSearchEditText.setOnTouchListener(null);
767 | InputMethodManager inputMethodManager = (InputMethodManager) getContext()
768 | .getSystemService(Context.INPUT_METHOD_SERVICE);
769 | inputMethodManager.toggleSoftInputFromWindow(
770 | getApplicationWindowToken(),
771 | InputMethodManager.SHOW_FORCED, 0);
772 | }
773 | }
774 | }
775 |
776 | private void closeSearchInternal() {
777 | this.mLogoView.setVisibility(View.VISIBLE);
778 | this.mSearchEditText.setVisibility(View.GONE);
779 | // if(mDisplayMode == DISPLAY_MODE_AS_TOOLBAR) {
780 | mSuggestionListView.setVisibility(View.GONE);
781 | // }
782 | // this.mSuggestionListView.setVisibility(View.GONE);
783 | /*if (mTintView != null && mRootLayout != null) {
784 | mRootLayout.removeView(mTintView);
785 | }*/
786 | if (mSearchListener != null)
787 | mSearchListener.onSearchEditClosed();
788 | showMicButton();
789 |
790 | hideKeyboard();
791 | }
792 |
793 | private void hideKeyboard() {
794 | if(showCustomKeyboard && mCustomKeyboardView != null) {
795 | mCustomKeyboardView.setVisibility(View.GONE);
796 | mCustomKeyboardView.setEnabled(false);
797 | }
798 | else {
799 | ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(getApplicationWindowToken(), 0);
800 | }
801 | }
802 |
803 | public boolean isEditing() {
804 | return mCurrentState == SearchViewState.EDITING;
805 | }
806 |
807 | public boolean isSearching() {
808 | return mCurrentState == SearchViewState.EDITING || mCurrentState == SearchViewState.SEARCH;
809 | }
810 |
811 | private void setLogoTextInt(String text) {
812 | mLogoView.setText(text);
813 | }
814 |
815 | public void setHomeButtonOpenIconState(HomeButton.IconState homeButtonOpenIconState) {
816 | this.mHomeButtonOpenIconState = homeButtonOpenIconState;
817 | }
818 |
819 | public void setHomeButtonCloseIconState(HomeButton.IconState homeButtonCloseIconState) {
820 | this.mHomeButtonCloseIconState = homeButtonCloseIconState;
821 | }
822 |
823 | public void setSuggestionBuilder(SearchSuggestionsBuilder suggestionBuilder) {
824 | this.mSuggestionBuilder = suggestionBuilder;
825 | }
826 |
827 | private void fromNormalToEditing() {
828 | if(mDisplayMode == DisplayMode.TOOLBAR) {
829 | setCurrentState(SearchViewState.EDITING);
830 | openSearchInternal(true);
831 | } else if(mDisplayMode == DisplayMode.MENUITEM) {
832 | setCurrentState(SearchViewState.EDITING);
833 | if(ViewCompat.isAttachedToWindow(this))
834 | revealFromMenuItem();
835 | else {
836 | getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
837 | @Override
838 | public void onGlobalLayout() {
839 | if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
840 | getViewTreeObserver().removeGlobalOnLayoutListener(this);
841 | } else {
842 | getViewTreeObserver().removeOnGlobalLayoutListener(this);
843 | }
844 | revealFromMenuItem();
845 | }
846 | });
847 | }
848 |
849 | }
850 | mHomeButton.animateState(mHomeButtonOpenIconState);
851 | }
852 |
853 | private void fromNormalToSearch() {
854 | if(mDisplayMode == DisplayMode.TOOLBAR) {
855 | setCurrentState(SearchViewState.SEARCH);
856 | search();
857 | } else if(mDisplayMode == DisplayMode.MENUITEM) {
858 | setVisibility(VISIBLE);
859 | fromEditingToSearch();
860 | }
861 | mHomeButton.animateState(mHomeButtonSearchIconState);
862 | }
863 |
864 | private void fromSearchToNormal() {
865 | setLogoTextInt("");
866 | setSearchString("", true);
867 | setCurrentState(SearchViewState.NORMAL);
868 | if(mDisplayMode == DisplayMode.TOOLBAR) {
869 | closeSearchInternal();
870 | } else if(mDisplayMode == DisplayMode.MENUITEM) {
871 | hideCircularlyToMenuItem();
872 | }
873 | setLogoTextInt("");
874 | if (mSearchListener != null)
875 | mSearchListener.onSearchExit();
876 | mHomeButton.animateState(mHomeButtonCloseIconState);
877 | }
878 |
879 | private void fromSearchToEditing() {
880 | openSearchInternal(true);
881 | setCurrentState(SearchViewState.EDITING);
882 | mHomeButton.animateState(mHomeButtonOpenIconState);
883 | }
884 |
885 | private void fromEditingToNormal() {
886 | setCurrentState(SearchViewState.NORMAL);
887 | if(mDisplayMode == DisplayMode.TOOLBAR) {
888 | setSearchString("", false);
889 | closeSearchInternal();
890 | } else if(mDisplayMode == DisplayMode.MENUITEM) {
891 | setSearchString("", false);
892 | hideCircularlyToMenuItem();
893 | }
894 | setLogoTextInt("");
895 | if (mSearchListener != null)
896 | mSearchListener.onSearchExit();
897 | mHomeButton.animateState(mHomeButtonCloseIconState);
898 | }
899 |
900 | private void fromEditingToSearch() {
901 | fromEditingToSearch(false, false);
902 | }
903 |
904 | private void fromEditingToSearch(boolean avoidSearch) {
905 | fromEditingToSearch(false, avoidSearch);
906 | }
907 |
908 | private void fromEditingToSearch(boolean forceSearch, boolean avoidSearch) {
909 | if(TextUtils.isEmpty(getSearchText())) {
910 | fromEditingToNormal();
911 | } else {
912 | setCurrentState(SearchViewState.SEARCH);
913 | if((!getSearchText().equals(mLogoView.getText()) || forceSearch) && !avoidSearch) {
914 | search();
915 | }
916 | closeSearchInternal();
917 | mHomeButton.animateState(mHomeButtonSearchIconState);
918 | }
919 | }
920 |
921 | private void dispatchStateChange(SearchViewState targetState) {
922 | if(targetState == SearchViewState.NORMAL) {
923 | if (mCurrentState == SearchViewState.EDITING) {
924 | fromEditingToNormal();
925 | } else if(mCurrentState == SearchViewState.SEARCH) {
926 | fromSearchToNormal();
927 | }
928 | } else if(targetState == SearchViewState.EDITING) {
929 | if (mCurrentState == SearchViewState.NORMAL) {
930 | fromNormalToEditing();
931 | } else if(mCurrentState == SearchViewState.SEARCH) {
932 | fromSearchToEditing();
933 | }
934 | } else if(targetState == SearchViewState.SEARCH) {
935 | if (mCurrentState == SearchViewState.NORMAL) {
936 | fromNormalToSearch();
937 | } else if(mCurrentState == SearchViewState.EDITING) {
938 | fromEditingToSearch();
939 | }
940 | }
941 | }
942 |
943 | private void setCurrentState(SearchViewState state) {
944 | mLastState = mCurrentState;
945 | mCurrentState = state;
946 | }
947 |
948 | public void openSearch() {
949 | dispatchStateChange(SearchViewState.EDITING);
950 | }
951 |
952 | public void setStartPositionFromMenuItem(View menuItemView) {
953 | DisplayMetrics metrics = getResources().getDisplayMetrics();
954 | int width = metrics.widthPixels;
955 | setStartPositionFromMenuItem(menuItemView, width);
956 | }
957 |
958 | public void setStartPositionFromMenuItem(View menuItemView, int desireRevealWidth) {
959 | if (menuItemView != null) {
960 | int[] location = new int[2];
961 | menuItemView.getLocationInWindow(location);
962 | int menuItemWidth = menuItemView.getWidth();
963 | this.mFromX = location[0] + menuItemWidth / 2;
964 | this.mFromY = location[1];
965 | this.mDesireRevealWidth = desireRevealWidth;
966 | }
967 | }
968 |
969 | public void openSearch(String query) {
970 | setSearchString(query, true);
971 | dispatchStateChange(SearchViewState.SEARCH);
972 | }
973 |
974 | public void closeSearch() {
975 | dispatchStateChange(SearchViewState.NORMAL);
976 | }
977 |
978 | public void cancelEditing() {
979 | if(TextUtils.isEmpty(mLogoView.getText())) {
980 | fromEditingToNormal();
981 | } else {
982 | fromEditingToSearch(true);
983 | }
984 | }
985 |
986 | public void setVoiceRecognitionDelegate(VoiceRecognitionDelegate delegate) {
987 | this.mVoiceRecognitionDelegate = delegate;
988 | micStateChanged();
989 | }
990 |
991 | public enum DisplayMode {
992 | MENUITEM(0), TOOLBAR(1);
993 | int mode;
994 |
995 | DisplayMode(int mode) {
996 | this.mode = mode;
997 | }
998 |
999 | public static DisplayMode fromInt(int mode) {
1000 | for (DisplayMode enumMode : values()) {
1001 | if (enumMode.mode == mode) return enumMode;
1002 | }
1003 | throw new IllegalArgumentException();
1004 | }
1005 |
1006 | public int toInt() {
1007 | return mode;
1008 | }
1009 | }
1010 |
1011 | public enum SearchViewState {
1012 | NORMAL(0), EDITING(1), SEARCH(2);
1013 | int state;
1014 | SearchViewState(int state) {
1015 | this.state = state;
1016 | }
1017 | public static SearchViewState fromInt(int state) {
1018 | for (SearchViewState enumState : values()) {
1019 | if (enumState.state == state) return enumState;
1020 | }
1021 | throw new IllegalArgumentException();
1022 | }
1023 |
1024 | public int toInt() {
1025 | return state;
1026 | }
1027 | }
1028 |
1029 | public interface SearchListener {
1030 |
1031 | /**
1032 | * Called when a suggestion is pressed is pressed
1033 | */
1034 | boolean onSuggestion(SearchItem searchItem);
1035 |
1036 | /**
1037 | * Called when the clear button is pressed
1038 | */
1039 | void onSearchCleared();
1040 |
1041 | /**
1042 | * Called when the PersistentSearchView's EditText text changes
1043 | */
1044 | void onSearchTermChanged(String term);
1045 |
1046 | /**
1047 | * Called when search happens
1048 | *
1049 | * @param query search string
1050 | */
1051 | void onSearch(String query);
1052 |
1053 | /**
1054 | * Called when search state change to SEARCH and EditText, Suggestions visible
1055 | */
1056 | void onSearchEditOpened();
1057 |
1058 | /**
1059 | * Called when search state change from SEARCH and EditText, Suggestions gone
1060 | */
1061 | void onSearchEditClosed();
1062 |
1063 | /**
1064 | * Called when edit text get focus and backpressed
1065 | */
1066 | boolean onSearchEditBackPressed();
1067 |
1068 | /**
1069 | * Called when search back to start state.
1070 | */
1071 | void onSearchExit();
1072 | }
1073 |
1074 | public interface HomeButtonListener {
1075 | /**
1076 | * Called when the menu button is pressed
1077 | */
1078 | void onHomeButtonClick();
1079 | }
1080 |
1081 | @Override
1082 | public Parcelable onSaveInstanceState() {
1083 | Parcelable superState = super.onSaveInstanceState();
1084 | SavedState ss = new SavedState(superState, mCurrentState);
1085 | ss.childrenStates = new SparseArray();
1086 | for (int i = 0; i < getChildCount(); i++) {
1087 | getChildAt(i).saveHierarchyState(ss.childrenStates);
1088 | }
1089 | return ss;
1090 | }
1091 |
1092 | @Override
1093 | public void onRestoreInstanceState(Parcelable state) {
1094 | if(!(state instanceof SavedState)) {
1095 | super.onRestoreInstanceState(state);
1096 | return;
1097 | }
1098 | this.mAvoidTriggerTextWatcher = true;
1099 | SavedState ss = (SavedState) state;
1100 | super.onRestoreInstanceState(ss.getSuperState());
1101 | for (int i = 0; i < getChildCount(); i++) {
1102 | getChildAt(i).restoreHierarchyState(ss.childrenStates);
1103 | }
1104 | dispatchStateChange(ss.getCurrentSearchViewState());
1105 | this.mAvoidTriggerTextWatcher = false;
1106 | }
1107 |
1108 | @Override
1109 | protected void dispatchSaveInstanceState(SparseArray container) {
1110 | dispatchFreezeSelfOnly(container);
1111 | }
1112 |
1113 | @Override
1114 | protected void dispatchRestoreInstanceState(SparseArray container) {
1115 | dispatchThawSelfOnly(container);
1116 | }
1117 |
1118 | static class SavedState extends BaseSavedState {
1119 | SparseArray childrenStates;
1120 | private SearchViewState mCurrentSearchViewState;
1121 |
1122 | SavedState(Parcelable superState, SearchViewState currentSearchViewState) {
1123 | super(superState);
1124 | mCurrentSearchViewState = currentSearchViewState;
1125 | }
1126 |
1127 | private SavedState(Parcel in, ClassLoader classLoader) {
1128 | super(in);
1129 | childrenStates = in.readSparseArray(classLoader);
1130 | mCurrentSearchViewState = SearchViewState.fromInt(in.readInt());
1131 | }
1132 |
1133 | @Override
1134 | public void writeToParcel(Parcel out, int flags) {
1135 | super.writeToParcel(out, flags);
1136 | out.writeSparseArray(childrenStates);
1137 | out.writeInt(mCurrentSearchViewState.toInt());
1138 | }
1139 |
1140 | public SearchViewState getCurrentSearchViewState() {
1141 | return mCurrentSearchViewState;
1142 | }
1143 |
1144 | public static final ClassLoaderCreator CREATOR
1145 | = new ClassLoaderCreator() {
1146 | @Override
1147 | public SavedState createFromParcel(Parcel source, ClassLoader loader) {
1148 | return new SavedState(source, loader);
1149 | }
1150 |
1151 | @Override
1152 | public SavedState createFromParcel(Parcel source) {
1153 | return createFromParcel(null);
1154 | }
1155 |
1156 | public SavedState[] newArray(int size) {
1157 | return new SavedState[size];
1158 | }
1159 | };
1160 | }
1161 | }
--------------------------------------------------------------------------------