blob: 5e6f3a46de7daa416a2b73c1372f55bb3b50a043 [file] [log] [blame]
Jim Miller24ccf3c2009-11-10 21:37:45 -08001/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.widget;
18
Artur Satayev2a9f3b82019-12-10 17:47:55 +000019import android.compat.annotation.UnsupportedAppUsage;
Jim Miller24ccf3c2009-11-10 21:37:45 -080020import android.content.Context;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Rect;
Jim Miller753401a2009-11-11 17:53:53 -080024import android.graphics.drawable.Drawable;
John Spurlock7b414672014-07-18 13:02:39 -040025import android.media.AudioAttributes;
Jeff Sharkey723a7252012-10-12 14:26:31 -070026import android.os.UserHandle;
Jim Miller24ccf3c2009-11-10 21:37:45 -080027import android.os.Vibrator;
Jeff Sharkey723a7252012-10-12 14:26:31 -070028import android.provider.Settings;
Jim Miller24ccf3c2009-11-10 21:37:45 -080029import android.util.AttributeSet;
30import android.util.Log;
31import android.view.Gravity;
32import android.view.MotionEvent;
33import android.view.View;
34import android.view.ViewGroup;
Jim Miller521d4002009-11-15 16:19:24 -080035import android.view.animation.AlphaAnimation;
36import android.view.animation.Animation;
Artur Satayev2a9f3b82019-12-10 17:47:55 +000037import android.view.animation.Animation.AnimationListener;
Jim Miller521d4002009-11-15 16:19:24 -080038import android.view.animation.LinearInterpolator;
39import android.view.animation.TranslateAnimation;
Jim Miller24ccf3c2009-11-10 21:37:45 -080040import android.widget.ImageView;
Jim Miller24ccf3c2009-11-10 21:37:45 -080041import android.widget.ImageView.ScaleType;
Artur Satayev2a9f3b82019-12-10 17:47:55 +000042import android.widget.TextView;
Jim Miller92c15712010-05-25 19:09:44 -070043
Jim Miller24ccf3c2009-11-10 21:37:45 -080044import com.android.internal.R;
45
46/**
47 * A special widget containing two Sliders and a threshold for each. Moving either slider beyond
Jim Miller425ca592009-11-13 19:20:28 -080048 * the threshold will cause the registered OnTriggerListener.onTrigger() to be called with
Jim Miller4df2c542009-11-12 03:39:14 -080049 * whichHandle being {@link OnTriggerListener#LEFT_HANDLE} or {@link OnTriggerListener#RIGHT_HANDLE}
Jim Miller425ca592009-11-13 19:20:28 -080050 * Equivalently, selecting a tab will result in a call to
Jim Miller4df2c542009-11-12 03:39:14 -080051 * {@link OnTriggerListener#onGrabbedStateChange(View, int)} with one of these two states. Releasing
52 * the tab will result in whichHandle being {@link OnTriggerListener#NO_HANDLE}.
Jim Miller24ccf3c2009-11-10 21:37:45 -080053 *
54 */
55public class SlidingTab extends ViewGroup {
56 private static final String LOG_TAG = "SlidingTab";
57 private static final boolean DBG = false;
58 private static final int HORIZONTAL = 0; // as defined in attrs.xml
59 private static final int VERTICAL = 1;
Jim Miller24ccf3c2009-11-10 21:37:45 -080060
61 // TODO: Make these configurable
Jim Miller4df2c542009-11-12 03:39:14 -080062 private static final float THRESHOLD = 2.0f / 3.0f;
Jim Miller24ccf3c2009-11-10 21:37:45 -080063 private static final long VIBRATE_SHORT = 30;
64 private static final long VIBRATE_LONG = 40;
Jim Miller521d4002009-11-15 16:19:24 -080065 private static final int TRACKING_MARGIN = 50;
66 private static final int ANIM_DURATION = 250; // Time for most animations (in ms)
67 private static final int ANIM_TARGET_TIME = 500; // Time to show targets (in ms)
Jim Miller4f01d4a2009-11-16 23:08:35 -080068 private boolean mHoldLeftOnTransition = true;
69 private boolean mHoldRightOnTransition = true;
Jim Miller24ccf3c2009-11-10 21:37:45 -080070
John Spurlock7b414672014-07-18 13:02:39 -040071 private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
72 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
73 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
74 .build();
75
Jim Miller24ccf3c2009-11-10 21:37:45 -080076 private OnTriggerListener mOnTriggerListener;
77 private int mGrabbedState = OnTriggerListener.NO_HANDLE;
78 private boolean mTriggered = false;
79 private Vibrator mVibrator;
Jim Miller92c15712010-05-25 19:09:44 -070080 private final float mDensity; // used to scale dimensions for bitmaps.
Jim Miller24ccf3c2009-11-10 21:37:45 -080081
Jim Miller24ccf3c2009-11-10 21:37:45 -080082 /**
83 * Either {@link #HORIZONTAL} or {@link #VERTICAL}.
84 */
Jim Miller92c15712010-05-25 19:09:44 -070085 private final int mOrientation;
Jim Miller24ccf3c2009-11-10 21:37:45 -080086
Andrei Oneaeecddd52019-03-27 10:32:55 +000087 @UnsupportedAppUsage
Jim Miller92c15712010-05-25 19:09:44 -070088 private final Slider mLeftSlider;
Andrei Oneaeecddd52019-03-27 10:32:55 +000089 @UnsupportedAppUsage
Jim Miller92c15712010-05-25 19:09:44 -070090 private final Slider mRightSlider;
Jim Miller24ccf3c2009-11-10 21:37:45 -080091 private Slider mCurrentSlider;
92 private boolean mTracking;
Jim Miller4df2c542009-11-12 03:39:14 -080093 private float mThreshold;
Jim Miller24ccf3c2009-11-10 21:37:45 -080094 private Slider mOtherSlider;
95 private boolean mAnimating;
Jim Miller92c15712010-05-25 19:09:44 -070096 private final Rect mTmpRect;
Jim Miller24ccf3c2009-11-10 21:37:45 -080097
98 /**
Jim Miller4f01d4a2009-11-16 23:08:35 -080099 * Listener used to reset the view when the current animation completes.
100 */
Andrei Oneaeecddd52019-03-27 10:32:55 +0000101 @UnsupportedAppUsage
Jim Miller4f01d4a2009-11-16 23:08:35 -0800102 private final AnimationListener mAnimationDoneListener = new AnimationListener() {
103 public void onAnimationStart(Animation animation) {
104
105 }
106
107 public void onAnimationRepeat(Animation animation) {
108
109 }
110
111 public void onAnimationEnd(Animation animation) {
112 onAnimationDone();
113 }
114 };
115
116 /**
Jim Miller24ccf3c2009-11-10 21:37:45 -0800117 * Interface definition for a callback to be invoked when a tab is triggered
Jim Miller4df2c542009-11-12 03:39:14 -0800118 * by moving it beyond a threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800119 */
120 public interface OnTriggerListener {
121 /**
122 * The interface was triggered because the user let go of the handle without reaching the
Jim Miller4df2c542009-11-12 03:39:14 -0800123 * threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800124 */
125 public static final int NO_HANDLE = 0;
126
127 /**
128 * The interface was triggered because the user grabbed the left handle and moved it past
Jim Miller4df2c542009-11-12 03:39:14 -0800129 * the threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800130 */
131 public static final int LEFT_HANDLE = 1;
132
133 /**
134 * The interface was triggered because the user grabbed the right handle and moved it past
Jim Miller4df2c542009-11-12 03:39:14 -0800135 * the threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800136 */
137 public static final int RIGHT_HANDLE = 2;
138
139 /**
Jim Miller4df2c542009-11-12 03:39:14 -0800140 * Called when the user moves a handle beyond the threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800141 *
142 * @param v The view that was triggered.
143 * @param whichHandle Which "dial handle" the user grabbed,
144 * either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}.
145 */
146 void onTrigger(View v, int whichHandle);
147
148 /**
149 * Called when the "grabbed state" changes (i.e. when the user either grabs or releases
150 * one of the handles.)
151 *
152 * @param v the view that was triggered
153 * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #LEFT_HANDLE},
154 * or {@link #RIGHT_HANDLE}.
155 */
156 void onGrabbedStateChange(View v, int grabbedState);
157 }
158
159 /**
Jim Miller425ca592009-11-13 19:20:28 -0800160 * Simple container class for all things pertinent to a slider.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800161 * A slider consists of 3 Views:
Jim Miller425ca592009-11-13 19:20:28 -0800162 *
Jim Miller24ccf3c2009-11-10 21:37:45 -0800163 * {@link #tab} is the tab shown on the screen in the default state.
164 * {@link #text} is the view revealed as the user slides the tab out.
165 * {@link #target} is the target the user must drag the slider past to trigger the slider.
166 *
167 */
168 private static class Slider {
169 /**
170 * Tab alignment - determines which side the tab should be drawn on
171 */
172 public static final int ALIGN_LEFT = 0;
173 public static final int ALIGN_RIGHT = 1;
174 public static final int ALIGN_TOP = 2;
175 public static final int ALIGN_BOTTOM = 3;
Jim Miller521d4002009-11-15 16:19:24 -0800176 public static final int ALIGN_UNKNOWN = 4;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800177
178 /**
179 * States for the view.
180 */
181 private static final int STATE_NORMAL = 0;
182 private static final int STATE_PRESSED = 1;
183 private static final int STATE_ACTIVE = 2;
184
Andrei Oneaeecddd52019-03-27 10:32:55 +0000185 @UnsupportedAppUsage
Jim Miller24ccf3c2009-11-10 21:37:45 -0800186 private final ImageView tab;
Andrei Oneaeecddd52019-03-27 10:32:55 +0000187 @UnsupportedAppUsage
Jim Miller24ccf3c2009-11-10 21:37:45 -0800188 private final TextView text;
189 private final ImageView target;
Jim Miller4df2c542009-11-12 03:39:14 -0800190 private int currentState = STATE_NORMAL;
Jim Miller521d4002009-11-15 16:19:24 -0800191 private int alignment = ALIGN_UNKNOWN;
192 private int alignment_value;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800193
194 /**
195 * Constructor
Jim Miller425ca592009-11-13 19:20:28 -0800196 *
Jim Miller24ccf3c2009-11-10 21:37:45 -0800197 * @param parent the container view of this one
198 * @param tabId drawable for the tab
199 * @param barId drawable for the bar
200 * @param targetId drawable for the target
201 */
202 Slider(ViewGroup parent, int tabId, int barId, int targetId) {
203 // Create tab
204 tab = new ImageView(parent.getContext());
205 tab.setBackgroundResource(tabId);
206 tab.setScaleType(ScaleType.CENTER);
207 tab.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
208 LayoutParams.WRAP_CONTENT));
209
210 // Create hint TextView
211 text = new TextView(parent.getContext());
212 text.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
Romain Guy980a9382010-01-08 15:06:28 -0800213 LayoutParams.MATCH_PARENT));
Jim Miller24ccf3c2009-11-10 21:37:45 -0800214 text.setBackgroundResource(barId);
215 text.setTextAppearance(parent.getContext(), R.style.TextAppearance_SlidingTabNormal);
216 // hint.setSingleLine(); // Hmm.. this causes the text to disappear off-screen
217
218 // Create target
219 target = new ImageView(parent.getContext());
220 target.setImageResource(targetId);
221 target.setScaleType(ScaleType.CENTER);
222 target.setLayoutParams(
223 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
224 target.setVisibility(View.INVISIBLE);
225
226 parent.addView(target); // this needs to be first - relies on painter's algorithm
227 parent.addView(tab);
228 parent.addView(text);
229 }
230
231 void setIcon(int iconId) {
232 tab.setImageResource(iconId);
233 }
Jim Miller425ca592009-11-13 19:20:28 -0800234
Jim Miller24ccf3c2009-11-10 21:37:45 -0800235 void setTabBackgroundResource(int tabId) {
236 tab.setBackgroundResource(tabId);
237 }
Jim Miller425ca592009-11-13 19:20:28 -0800238
Jim Miller24ccf3c2009-11-10 21:37:45 -0800239 void setBarBackgroundResource(int barId) {
240 text.setBackgroundResource(barId);
241 }
Jim Miller425ca592009-11-13 19:20:28 -0800242
Jim Miller24ccf3c2009-11-10 21:37:45 -0800243 void setHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800244 text.setText(resId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800245 }
246
247 void hide() {
Jim Miller521d4002009-11-15 16:19:24 -0800248 boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
249 int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getRight()
250 : alignment_value - tab.getLeft()) : 0;
251 int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getBottom()
252 : alignment_value - tab.getTop());
253
254 Animation trans = new TranslateAnimation(0, dx, 0, dy);
255 trans.setDuration(ANIM_DURATION);
256 trans.setFillAfter(true);
257 tab.startAnimation(trans);
258 text.startAnimation(trans);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800259 target.setVisibility(View.INVISIBLE);
260 }
261
Jim Miller521d4002009-11-15 16:19:24 -0800262 void show(boolean animate) {
263 text.setVisibility(View.VISIBLE);
264 tab.setVisibility(View.VISIBLE);
265 //target.setVisibility(View.INVISIBLE);
266 if (animate) {
267 boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
268 int dx = horiz ? (alignment == ALIGN_LEFT ? tab.getWidth() : -tab.getWidth()) : 0;
269 int dy = horiz ? 0: (alignment == ALIGN_TOP ? tab.getHeight() : -tab.getHeight());
270
271 Animation trans = new TranslateAnimation(-dx, 0, -dy, 0);
272 trans.setDuration(ANIM_DURATION);
273 tab.startAnimation(trans);
274 text.startAnimation(trans);
275 }
276 }
277
Jim Miller24ccf3c2009-11-10 21:37:45 -0800278 void setState(int state) {
279 text.setPressed(state == STATE_PRESSED);
280 tab.setPressed(state == STATE_PRESSED);
281 if (state == STATE_ACTIVE) {
282 final int[] activeState = new int[] {com.android.internal.R.attr.state_active};
283 if (text.getBackground().isStateful()) {
284 text.getBackground().setState(activeState);
285 }
286 if (tab.getBackground().isStateful()) {
287 tab.getBackground().setState(activeState);
288 }
289 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabActive);
290 } else {
291 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
292 }
Jim Miller4df2c542009-11-12 03:39:14 -0800293 currentState = state;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800294 }
295
296 void showTarget() {
Jim Miller521d4002009-11-15 16:19:24 -0800297 AlphaAnimation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
298 alphaAnim.setDuration(ANIM_TARGET_TIME);
299 target.startAnimation(alphaAnim);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800300 target.setVisibility(View.VISIBLE);
301 }
302
Jim Miller521d4002009-11-15 16:19:24 -0800303 void reset(boolean animate) {
Jim Miller24ccf3c2009-11-10 21:37:45 -0800304 setState(STATE_NORMAL);
305 text.setVisibility(View.VISIBLE);
306 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
307 tab.setVisibility(View.VISIBLE);
308 target.setVisibility(View.INVISIBLE);
Jim Miller521d4002009-11-15 16:19:24 -0800309 final boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
310 int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getLeft()
311 : alignment_value - tab.getRight()) : 0;
312 int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getTop()
313 : alignment_value - tab.getBottom());
314 if (animate) {
315 TranslateAnimation trans = new TranslateAnimation(0, dx, 0, dy);
316 trans.setDuration(ANIM_DURATION);
317 trans.setFillAfter(false);
318 text.startAnimation(trans);
319 tab.startAnimation(trans);
320 } else {
321 if (horiz) {
322 text.offsetLeftAndRight(dx);
323 tab.offsetLeftAndRight(dx);
324 } else {
325 text.offsetTopAndBottom(dy);
326 tab.offsetTopAndBottom(dy);
327 }
Jim Miller4f01d4a2009-11-16 23:08:35 -0800328 text.clearAnimation();
329 tab.clearAnimation();
330 target.clearAnimation();
Jim Miller521d4002009-11-15 16:19:24 -0800331 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800332 }
333
334 void setTarget(int targetId) {
335 target.setImageResource(targetId);
336 }
337
338 /**
339 * Layout the given widgets within the parent.
340 *
341 * @param l the parent's left border
342 * @param t the parent's top border
343 * @param r the parent's right border
344 * @param b the parent's bottom border
345 * @param alignment which side to align the widget to
346 */
347 void layout(int l, int t, int r, int b, int alignment) {
Jim Miller521d4002009-11-15 16:19:24 -0800348 this.alignment = alignment;
Jim Miller753401a2009-11-11 17:53:53 -0800349 final Drawable tabBackground = tab.getBackground();
350 final int handleWidth = tabBackground.getIntrinsicWidth();
351 final int handleHeight = tabBackground.getIntrinsicHeight();
352 final Drawable targetDrawable = target.getDrawable();
353 final int targetWidth = targetDrawable.getIntrinsicWidth();
354 final int targetHeight = targetDrawable.getIntrinsicHeight();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800355 final int parentWidth = r - l;
356 final int parentHeight = b - t;
357
Jim Miller4df2c542009-11-12 03:39:14 -0800358 final int leftTarget = (int) (THRESHOLD * parentWidth) - targetWidth + handleWidth / 2;
359 final int rightTarget = (int) ((1.0f - THRESHOLD) * parentWidth) - handleWidth / 2;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800360 final int left = (parentWidth - handleWidth) / 2;
361 final int right = left + handleWidth;
362
363 if (alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT) {
364 // horizontal
365 final int targetTop = (parentHeight - targetHeight) / 2;
366 final int targetBottom = targetTop + targetHeight;
367 final int top = (parentHeight - handleHeight) / 2;
368 final int bottom = (parentHeight + handleHeight) / 2;
369 if (alignment == ALIGN_LEFT) {
370 tab.layout(0, top, handleWidth, bottom);
371 text.layout(0 - parentWidth, top, 0, bottom);
372 text.setGravity(Gravity.RIGHT);
373 target.layout(leftTarget, targetTop, leftTarget + targetWidth, targetBottom);
Jim Miller521d4002009-11-15 16:19:24 -0800374 alignment_value = l;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800375 } else {
376 tab.layout(parentWidth - handleWidth, top, parentWidth, bottom);
377 text.layout(parentWidth, top, parentWidth + parentWidth, bottom);
378 target.layout(rightTarget, targetTop, rightTarget + targetWidth, targetBottom);
379 text.setGravity(Gravity.TOP);
Jim Miller521d4002009-11-15 16:19:24 -0800380 alignment_value = r;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800381 }
382 } else {
383 // vertical
384 final int targetLeft = (parentWidth - targetWidth) / 2;
385 final int targetRight = (parentWidth + targetWidth) / 2;
Jim Miller4df2c542009-11-12 03:39:14 -0800386 final int top = (int) (THRESHOLD * parentHeight) + handleHeight / 2 - targetHeight;
387 final int bottom = (int) ((1.0f - THRESHOLD) * parentHeight) - handleHeight / 2;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800388 if (alignment == ALIGN_TOP) {
389 tab.layout(left, 0, right, handleHeight);
390 text.layout(left, 0 - parentHeight, right, 0);
391 target.layout(targetLeft, top, targetRight, top + targetHeight);
Jim Miller521d4002009-11-15 16:19:24 -0800392 alignment_value = t;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800393 } else {
394 tab.layout(left, parentHeight - handleHeight, right, parentHeight);
395 text.layout(left, parentHeight, right, parentHeight + parentHeight);
396 target.layout(targetLeft, bottom, targetRight, bottom + targetHeight);
Jim Miller521d4002009-11-15 16:19:24 -0800397 alignment_value = b;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800398 }
399 }
400 }
401
Jim Miller4df2c542009-11-12 03:39:14 -0800402 public void updateDrawableStates() {
403 setState(currentState);
404 }
405
Jim Miller4811d622009-11-12 17:45:07 -0800406 /**
407 * Ensure all the dependent widgets are measured.
408 */
Filip Gruszczynskib6824bf2015-04-13 09:16:25 -0700409 public void measure(int widthMeasureSpec, int heightMeasureSpec) {
410 int width = MeasureSpec.getSize(widthMeasureSpec);
411 int height = MeasureSpec.getSize(heightMeasureSpec);
Adam Powelld5dbf4b2015-06-11 13:19:24 -0700412 tab.measure(View.MeasureSpec.makeSafeMeasureSpec(width, View.MeasureSpec.UNSPECIFIED),
413 View.MeasureSpec.makeSafeMeasureSpec(height, View.MeasureSpec.UNSPECIFIED));
414 text.measure(View.MeasureSpec.makeSafeMeasureSpec(width, View.MeasureSpec.UNSPECIFIED),
415 View.MeasureSpec.makeSafeMeasureSpec(height, View.MeasureSpec.UNSPECIFIED));
Jim Miller24ccf3c2009-11-10 21:37:45 -0800416 }
417
Jim Miller4811d622009-11-12 17:45:07 -0800418 /**
419 * Get the measured tab width. Must be called after {@link Slider#measure()}.
420 * @return
421 */
422 public int getTabWidth() {
423 return tab.getMeasuredWidth();
424 }
425
426 /**
427 * Get the measured tab width. Must be called after {@link Slider#measure()}.
428 * @return
429 */
Jim Miller24ccf3c2009-11-10 21:37:45 -0800430 public int getTabHeight() {
Jim Miller4811d622009-11-12 17:45:07 -0800431 return tab.getMeasuredHeight();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800432 }
Jim Miller521d4002009-11-15 16:19:24 -0800433
Jim Miller8b63ab62009-12-02 13:42:48 -0800434 /**
Ken Wakasaf76a50c2012-03-09 19:56:35 +0900435 * Start animating the slider. Note we need two animations since a ValueAnimator
Jim Miller8b63ab62009-12-02 13:42:48 -0800436 * keeps internal state of the invalidation region which is just the view being animated.
Jim Miller2cd1e6e2010-01-15 16:20:36 -0800437 *
Jim Miller8b63ab62009-12-02 13:42:48 -0800438 * @param anim1
439 * @param anim2
440 */
441 public void startAnimation(Animation anim1, Animation anim2) {
442 tab.startAnimation(anim1);
443 text.startAnimation(anim2);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800444 }
445
446 public void hideTarget() {
447 target.clearAnimation();
448 target.setVisibility(View.INVISIBLE);
Jim Miller521d4002009-11-15 16:19:24 -0800449 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800450 }
451
452 public SlidingTab(Context context) {
453 this(context, null);
454 }
455
456 /**
457 * Constructor used when this widget is created from a layout file.
458 */
459 public SlidingTab(Context context, AttributeSet attrs) {
460 super(context, attrs);
461
Jim Miller521d4002009-11-15 16:19:24 -0800462 // Allocate a temporary once that can be used everywhere.
463 mTmpRect = new Rect();
464
Jim Miller24ccf3c2009-11-10 21:37:45 -0800465 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab);
466 mOrientation = a.getInt(R.styleable.SlidingTab_orientation, HORIZONTAL);
467 a.recycle();
468
469 Resources r = getResources();
470 mDensity = r.getDisplayMetrics().density;
471 if (DBG) log("- Density: " + mDensity);
472
Jim Miller425ca592009-11-13 19:20:28 -0800473 mLeftSlider = new Slider(this,
474 R.drawable.jog_tab_left_generic,
Jim Miller24ccf3c2009-11-10 21:37:45 -0800475 R.drawable.jog_tab_bar_left_generic,
476 R.drawable.jog_tab_target_gray);
Jim Miller425ca592009-11-13 19:20:28 -0800477 mRightSlider = new Slider(this,
478 R.drawable.jog_tab_right_generic,
Jim Miller24ccf3c2009-11-10 21:37:45 -0800479 R.drawable.jog_tab_bar_right_generic,
480 R.drawable.jog_tab_target_gray);
481
482 // setBackgroundColor(0x80808080);
483 }
484
485 @Override
486 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
487 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
488 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
489
490 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
491 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
492
Jim Miller3747f3f2010-08-17 13:31:46 -0700493 if (DBG) {
494 if (widthSpecMode == MeasureSpec.UNSPECIFIED
495 || heightSpecMode == MeasureSpec.UNSPECIFIED) {
496 Log.e("SlidingTab", "SlidingTab cannot have UNSPECIFIED MeasureSpec"
497 +"(wspec=" + widthSpecMode + ", hspec=" + heightSpecMode + ")",
498 new RuntimeException(LOG_TAG + "stack:"));
499 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800500 }
501
Filip Gruszczynskib6824bf2015-04-13 09:16:25 -0700502 mLeftSlider.measure(widthMeasureSpec, heightMeasureSpec);
503 mRightSlider.measure(widthMeasureSpec, heightMeasureSpec);
Jim Miller4811d622009-11-12 17:45:07 -0800504 final int leftTabWidth = mLeftSlider.getTabWidth();
505 final int rightTabWidth = mRightSlider.getTabWidth();
506 final int leftTabHeight = mLeftSlider.getTabHeight();
507 final int rightTabHeight = mRightSlider.getTabHeight();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800508 final int width;
509 final int height;
510 if (isHorizontal()) {
511 width = Math.max(widthSpecSize, leftTabWidth + rightTabWidth);
512 height = Math.max(leftTabHeight, rightTabHeight);
513 } else {
514 width = Math.max(leftTabWidth, rightTabHeight);
515 height = Math.max(heightSpecSize, leftTabHeight + rightTabHeight);
516 }
517 setMeasuredDimension(width, height);
518 }
519
520 @Override
521 public boolean onInterceptTouchEvent(MotionEvent event) {
522 final int action = event.getAction();
523 final float x = event.getX();
524 final float y = event.getY();
525
Jim Miller24ccf3c2009-11-10 21:37:45 -0800526 if (mAnimating) {
527 return false;
528 }
529
530 View leftHandle = mLeftSlider.tab;
Jim Miller521d4002009-11-15 16:19:24 -0800531 leftHandle.getHitRect(mTmpRect);
532 boolean leftHit = mTmpRect.contains((int) x, (int) y);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800533
534 View rightHandle = mRightSlider.tab;
Jim Miller521d4002009-11-15 16:19:24 -0800535 rightHandle.getHitRect(mTmpRect);
536 boolean rightHit = mTmpRect.contains((int)x, (int) y);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800537
538 if (!mTracking && !(leftHit || rightHit)) {
539 return false;
540 }
541
542 switch (action) {
543 case MotionEvent.ACTION_DOWN: {
544 mTracking = true;
545 mTriggered = false;
546 vibrate(VIBRATE_SHORT);
547 if (leftHit) {
548 mCurrentSlider = mLeftSlider;
549 mOtherSlider = mRightSlider;
Jim Miller4df2c542009-11-12 03:39:14 -0800550 mThreshold = isHorizontal() ? THRESHOLD : 1.0f - THRESHOLD;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800551 setGrabbedState(OnTriggerListener.LEFT_HANDLE);
552 } else {
553 mCurrentSlider = mRightSlider;
554 mOtherSlider = mLeftSlider;
Jim Miller4df2c542009-11-12 03:39:14 -0800555 mThreshold = isHorizontal() ? 1.0f - THRESHOLD : THRESHOLD;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800556 setGrabbedState(OnTriggerListener.RIGHT_HANDLE);
557 }
558 mCurrentSlider.setState(Slider.STATE_PRESSED);
559 mCurrentSlider.showTarget();
560 mOtherSlider.hide();
561 break;
562 }
563 }
564
565 return true;
566 }
567
Jim Millerd8a3a892010-01-29 17:25:26 -0800568 /**
569 * Reset the tabs to their original state and stop any existing animation.
570 * Animate them back into place if animate is true.
571 *
572 * @param animate
573 */
574 public void reset(boolean animate) {
575 mLeftSlider.reset(animate);
576 mRightSlider.reset(animate);
Jim Miller966a8c72010-03-26 15:54:09 -0700577 if (!animate) {
578 mAnimating = false;
579 }
Jim Millerd8a3a892010-01-29 17:25:26 -0800580 }
581
Jim Miller24ccf3c2009-11-10 21:37:45 -0800582 @Override
Jim Miller4f01d4a2009-11-16 23:08:35 -0800583 public void setVisibility(int visibility) {
584 // Clear animations so sliders don't continue to animate when we show the widget again.
585 if (visibility != getVisibility() && visibility == View.INVISIBLE) {
Jim Millerd8a3a892010-01-29 17:25:26 -0800586 reset(false);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800587 }
588 super.setVisibility(visibility);
589 }
590
591 @Override
Jim Miller24ccf3c2009-11-10 21:37:45 -0800592 public boolean onTouchEvent(MotionEvent event) {
593 if (mTracking) {
594 final int action = event.getAction();
595 final float x = event.getX();
596 final float y = event.getY();
Jim Miller521d4002009-11-15 16:19:24 -0800597
Jim Miller24ccf3c2009-11-10 21:37:45 -0800598 switch (action) {
599 case MotionEvent.ACTION_MOVE:
Jim Miller521d4002009-11-15 16:19:24 -0800600 if (withinView(x, y, this) ) {
601 moveHandle(x, y);
602 float position = isHorizontal() ? x : y;
603 float target = mThreshold * (isHorizontal() ? getWidth() : getHeight());
604 boolean thresholdReached;
605 if (isHorizontal()) {
606 thresholdReached = mCurrentSlider == mLeftSlider ?
607 position > target : position < target;
608 } else {
609 thresholdReached = mCurrentSlider == mLeftSlider ?
610 position < target : position > target;
611 }
612 if (!mTriggered && thresholdReached) {
613 mTriggered = true;
614 mTracking = false;
615 mCurrentSlider.setState(Slider.STATE_ACTIVE);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800616 boolean isLeft = mCurrentSlider == mLeftSlider;
617 dispatchTriggerEvent(isLeft ?
Jim Miller521d4002009-11-15 16:19:24 -0800618 OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800619
Jim Miller4f01d4a2009-11-16 23:08:35 -0800620 startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition);
Jim Miller521d4002009-11-15 16:19:24 -0800621 setGrabbedState(OnTriggerListener.NO_HANDLE);
622 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800623 break;
624 }
625 // Intentionally fall through - we're outside tracking rectangle
626
627 case MotionEvent.ACTION_UP:
628 case MotionEvent.ACTION_CANCEL:
Jim Miller92c15712010-05-25 19:09:44 -0700629 cancelGrab();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800630 break;
631 }
632 }
633
634 return mTracking || super.onTouchEvent(event);
635 }
636
Jim Miller92c15712010-05-25 19:09:44 -0700637 private void cancelGrab() {
638 mTracking = false;
639 mTriggered = false;
640 mOtherSlider.show(true);
641 mCurrentSlider.reset(false);
642 mCurrentSlider.hideTarget();
643 mCurrentSlider = null;
644 mOtherSlider = null;
645 setGrabbedState(OnTriggerListener.NO_HANDLE);
646 }
647
Jim Miller4f01d4a2009-11-16 23:08:35 -0800648 void startAnimating(final boolean holdAfter) {
Jim Miller521d4002009-11-15 16:19:24 -0800649 mAnimating = true;
Jim Miller8b63ab62009-12-02 13:42:48 -0800650 final Animation trans1;
651 final Animation trans2;
Jim Miller4f01d4a2009-11-16 23:08:35 -0800652 final Slider slider = mCurrentSlider;
653 final Slider other = mOtherSlider;
654 final int dx;
655 final int dy;
Jim Miller521d4002009-11-15 16:19:24 -0800656 if (isHorizontal()) {
657 int right = slider.tab.getRight();
658 int width = slider.tab.getWidth();
659 int left = slider.tab.getLeft();
660 int viewWidth = getWidth();
Jim Miller4f01d4a2009-11-16 23:08:35 -0800661 int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim
662 dx = slider == mRightSlider ? - (right + viewWidth - holdOffset)
663 : (viewWidth - left) + viewWidth - holdOffset;
Jim Miller521d4002009-11-15 16:19:24 -0800664 dy = 0;
665 } else {
666 int top = slider.tab.getTop();
667 int bottom = slider.tab.getBottom();
668 int height = slider.tab.getHeight();
669 int viewHeight = getHeight();
Jim Miller4f01d4a2009-11-16 23:08:35 -0800670 int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim
Jim Miller521d4002009-11-15 16:19:24 -0800671 dx = 0;
Jim Miller4f01d4a2009-11-16 23:08:35 -0800672 dy = slider == mRightSlider ? (top + viewHeight - holdOffset)
673 : - ((viewHeight - bottom) + viewHeight - holdOffset);
Jim Miller521d4002009-11-15 16:19:24 -0800674 }
Jim Miller8b63ab62009-12-02 13:42:48 -0800675 trans1 = new TranslateAnimation(0, dx, 0, dy);
676 trans1.setDuration(ANIM_DURATION);
677 trans1.setInterpolator(new LinearInterpolator());
678 trans1.setFillAfter(true);
679 trans2 = new TranslateAnimation(0, dx, 0, dy);
680 trans2.setDuration(ANIM_DURATION);
681 trans2.setInterpolator(new LinearInterpolator());
682 trans2.setFillAfter(true);
Jim Miller521d4002009-11-15 16:19:24 -0800683
Jim Miller8b63ab62009-12-02 13:42:48 -0800684 trans1.setAnimationListener(new AnimationListener() {
Jim Miller521d4002009-11-15 16:19:24 -0800685 public void onAnimationEnd(Animation animation) {
Jim Miller4f01d4a2009-11-16 23:08:35 -0800686 Animation anim;
687 if (holdAfter) {
688 anim = new TranslateAnimation(dx, dx, dy, dy);
689 anim.setDuration(1000); // plenty of time for transitions
690 mAnimating = false;
691 } else {
692 anim = new AlphaAnimation(0.5f, 1.0f);
693 anim.setDuration(ANIM_DURATION);
694 resetView();
695 }
696 anim.setAnimationListener(mAnimationDoneListener);
Jim Miller2cd1e6e2010-01-15 16:20:36 -0800697
Jim Miller8b63ab62009-12-02 13:42:48 -0800698 /* Animation can be the same for these since the animation just holds */
699 mLeftSlider.startAnimation(anim, anim);
700 mRightSlider.startAnimation(anim, anim);
Jim Miller521d4002009-11-15 16:19:24 -0800701 }
702
703 public void onAnimationRepeat(Animation animation) {
704
705 }
706
707 public void onAnimationStart(Animation animation) {
708
709 }
710
711 });
712
Jim Miller4f01d4a2009-11-16 23:08:35 -0800713 slider.hideTarget();
Jim Miller8b63ab62009-12-02 13:42:48 -0800714 slider.startAnimation(trans1, trans2);
Jim Miller521d4002009-11-15 16:19:24 -0800715 }
716
Andrei Oneaeecddd52019-03-27 10:32:55 +0000717 @UnsupportedAppUsage
Jim Miller4f01d4a2009-11-16 23:08:35 -0800718 private void onAnimationDone() {
719 resetView();
720 mAnimating = false;
721 }
722
Jim Miller521d4002009-11-15 16:19:24 -0800723 private boolean withinView(final float x, final float y, final View view) {
724 return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight()
725 || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth();
726 }
727
Jim Miller24ccf3c2009-11-10 21:37:45 -0800728 private boolean isHorizontal() {
729 return mOrientation == HORIZONTAL;
730 }
731
Andrei Oneaeecddd52019-03-27 10:32:55 +0000732 @UnsupportedAppUsage
Jim Miller24ccf3c2009-11-10 21:37:45 -0800733 private void resetView() {
Jim Miller521d4002009-11-15 16:19:24 -0800734 mLeftSlider.reset(false);
735 mRightSlider.reset(false);
736 // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight());
Jim Miller24ccf3c2009-11-10 21:37:45 -0800737 }
738
739 @Override
740 protected void onLayout(boolean changed, int l, int t, int r, int b) {
741 if (!changed) return;
742
743 // Center the widgets in the view
744 mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM);
745 mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800746 }
747
748 private void moveHandle(float x, float y) {
749 final View handle = mCurrentSlider.tab;
750 final View content = mCurrentSlider.text;
751 if (isHorizontal()) {
752 int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2);
753 handle.offsetLeftAndRight(deltaX);
754 content.offsetLeftAndRight(deltaX);
755 } else {
756 int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2);
757 handle.offsetTopAndBottom(deltaY);
758 content.offsetTopAndBottom(deltaY);
759 }
760 invalidate(); // TODO: be more conservative about what we're invalidating
761 }
762
763 /**
764 * Sets the left handle icon to a given resource.
765 *
766 * The resource should refer to a Drawable object, or use 0 to remove
767 * the icon.
768 *
769 * @param iconId the resource ID of the icon drawable
770 * @param targetId the resource of the target drawable
771 * @param barId the resource of the bar drawable (stateful)
Jim Miller425ca592009-11-13 19:20:28 -0800772 * @param tabId the resource of the
Jim Miller24ccf3c2009-11-10 21:37:45 -0800773 */
Andrei Oneaeecddd52019-03-27 10:32:55 +0000774 @UnsupportedAppUsage
Jim Miller24ccf3c2009-11-10 21:37:45 -0800775 public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) {
776 mLeftSlider.setIcon(iconId);
Jim Miller425ca592009-11-13 19:20:28 -0800777 mLeftSlider.setTarget(targetId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800778 mLeftSlider.setBarBackgroundResource(barId);
779 mLeftSlider.setTabBackgroundResource(tabId);
Jim Miller4df2c542009-11-12 03:39:14 -0800780 mLeftSlider.updateDrawableStates();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800781 }
782
783 /**
784 * Sets the left handle hint text to a given resource string.
785 *
786 * @param resId
787 */
Andrei Oneaeecddd52019-03-27 10:32:55 +0000788 @UnsupportedAppUsage
Jim Miller24ccf3c2009-11-10 21:37:45 -0800789 public void setLeftHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800790 if (isHorizontal()) {
791 mLeftSlider.setHintText(resId);
792 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800793 }
794
795 /**
796 * Sets the right handle icon to a given resource.
797 *
798 * The resource should refer to a Drawable object, or use 0 to remove
799 * the icon.
800 *
801 * @param iconId the resource ID of the icon drawable
802 * @param targetId the resource of the target drawable
803 * @param barId the resource of the bar drawable (stateful)
Jim Miller425ca592009-11-13 19:20:28 -0800804 * @param tabId the resource of the
Jim Miller24ccf3c2009-11-10 21:37:45 -0800805 */
Andrei Oneaeecddd52019-03-27 10:32:55 +0000806 @UnsupportedAppUsage
Jim Miller24ccf3c2009-11-10 21:37:45 -0800807 public void setRightTabResources(int iconId, int targetId, int barId, int tabId) {
808 mRightSlider.setIcon(iconId);
Jim Miller425ca592009-11-13 19:20:28 -0800809 mRightSlider.setTarget(targetId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800810 mRightSlider.setBarBackgroundResource(barId);
811 mRightSlider.setTabBackgroundResource(tabId);
Jim Miller4df2c542009-11-12 03:39:14 -0800812 mRightSlider.updateDrawableStates();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800813 }
814
815 /**
816 * Sets the left handle hint text to a given resource string.
817 *
818 * @param resId
819 */
Andrei Oneaeecddd52019-03-27 10:32:55 +0000820 @UnsupportedAppUsage
Jim Miller24ccf3c2009-11-10 21:37:45 -0800821 public void setRightHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800822 if (isHorizontal()) {
823 mRightSlider.setHintText(resId);
824 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800825 }
826
Andrei Oneaeecddd52019-03-27 10:32:55 +0000827 @UnsupportedAppUsage
Jim Miller4f01d4a2009-11-16 23:08:35 -0800828 public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) {
829 mHoldLeftOnTransition = holdLeft;
830 mHoldRightOnTransition = holdRight;
831 }
832
Jim Miller24ccf3c2009-11-10 21:37:45 -0800833 /**
834 * Triggers haptic feedback.
835 */
836 private synchronized void vibrate(long duration) {
Jeff Sharkey723a7252012-10-12 14:26:31 -0700837 final boolean hapticEnabled = Settings.System.getIntForUser(
838 mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
839 UserHandle.USER_CURRENT) != 0;
840 if (hapticEnabled) {
841 if (mVibrator == null) {
842 mVibrator = (android.os.Vibrator) getContext()
843 .getSystemService(Context.VIBRATOR_SERVICE);
844 }
John Spurlock7b414672014-07-18 13:02:39 -0400845 mVibrator.vibrate(duration, VIBRATION_ATTRIBUTES);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800846 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800847 }
848
849 /**
850 * Registers a callback to be invoked when the user triggers an event.
851 *
852 * @param listener the OnDialTriggerListener to attach to this view
853 */
Andrei Oneaeecddd52019-03-27 10:32:55 +0000854 @UnsupportedAppUsage
Jim Miller24ccf3c2009-11-10 21:37:45 -0800855 public void setOnTriggerListener(OnTriggerListener listener) {
856 mOnTriggerListener = listener;
857 }
858
859 /**
860 * Dispatches a trigger event to listener. Ignored if a listener is not set.
861 * @param whichHandle the handle that triggered the event.
862 */
863 private void dispatchTriggerEvent(int whichHandle) {
864 vibrate(VIBRATE_LONG);
865 if (mOnTriggerListener != null) {
866 mOnTriggerListener.onTrigger(this, whichHandle);
867 }
868 }
869
Jim Miller92c15712010-05-25 19:09:44 -0700870 @Override
871 protected void onVisibilityChanged(View changedView, int visibility) {
872 super.onVisibilityChanged(changedView, visibility);
873 // When visibility changes and the user has a tab selected, unselect it and
874 // make sure their callback gets called.
875 if (changedView == this && visibility != VISIBLE
876 && mGrabbedState != OnTriggerListener.NO_HANDLE) {
877 cancelGrab();
878 }
879 }
880
Jim Miller24ccf3c2009-11-10 21:37:45 -0800881 /**
882 * Sets the current grabbed state, and dispatches a grabbed state change
883 * event to our listener.
884 */
885 private void setGrabbedState(int newState) {
886 if (newState != mGrabbedState) {
887 mGrabbedState = newState;
888 if (mOnTriggerListener != null) {
889 mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
890 }
891 }
892 }
893
Jim Miller24ccf3c2009-11-10 21:37:45 -0800894 private void log(String msg) {
895 Log.d(LOG_TAG, msg);
896 }
897}