diff --git a/res/drawable-hdpi/jog_tab_bar_left_end_confirm_green.9.png b/res/drawable-hdpi/jog_tab_bar_left_end_confirm_green.9.png
new file mode 100644
index 000000000..7c4f40ea9
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_bar_left_end_confirm_green.9.png differ
diff --git a/res/drawable-hdpi/jog_tab_bar_left_end_normal.9.png b/res/drawable-hdpi/jog_tab_bar_left_end_normal.9.png
new file mode 100644
index 000000000..b9ec2374a
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_bar_left_end_normal.9.png differ
diff --git a/res/drawable-hdpi/jog_tab_bar_left_end_pressed.9.png b/res/drawable-hdpi/jog_tab_bar_left_end_pressed.9.png
new file mode 100644
index 000000000..2800cabeb
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_bar_left_end_pressed.9.png differ
diff --git a/res/drawable-hdpi/jog_tab_bar_right_end_confirm_red.9.png b/res/drawable-hdpi/jog_tab_bar_right_end_confirm_red.9.png
new file mode 100644
index 000000000..fd98571d3
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_bar_right_end_confirm_red.9.png differ
diff --git a/res/drawable-hdpi/jog_tab_bar_right_end_normal.9.png b/res/drawable-hdpi/jog_tab_bar_right_end_normal.9.png
new file mode 100644
index 000000000..49ec1184f
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_bar_right_end_normal.9.png differ
diff --git a/res/drawable-hdpi/jog_tab_bar_right_end_pressed.9.png b/res/drawable-hdpi/jog_tab_bar_right_end_pressed.9.png
new file mode 100644
index 000000000..ffc54339d
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_bar_right_end_pressed.9.png differ
diff --git a/res/drawable-hdpi/jog_tab_left_confirm_green.png b/res/drawable-hdpi/jog_tab_left_confirm_green.png
new file mode 100644
index 000000000..23acc95f1
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_left_confirm_green.png differ
diff --git a/res/drawable-hdpi/jog_tab_left_normal.png b/res/drawable-hdpi/jog_tab_left_normal.png
new file mode 100644
index 000000000..18216f946
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_left_normal.png differ
diff --git a/res/drawable-hdpi/jog_tab_left_pressed.png b/res/drawable-hdpi/jog_tab_left_pressed.png
new file mode 100644
index 000000000..86f49c685
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_left_pressed.png differ
diff --git a/res/drawable-hdpi/jog_tab_right_confirm_red.png b/res/drawable-hdpi/jog_tab_right_confirm_red.png
new file mode 100644
index 000000000..bb0fa4754
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_right_confirm_red.png differ
diff --git a/res/drawable-hdpi/jog_tab_right_normal.png b/res/drawable-hdpi/jog_tab_right_normal.png
new file mode 100644
index 000000000..68545ef51
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_right_normal.png differ
diff --git a/res/drawable-hdpi/jog_tab_right_pressed.png b/res/drawable-hdpi/jog_tab_right_pressed.png
new file mode 100644
index 000000000..3d75c0d9b
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_right_pressed.png differ
diff --git a/res/drawable-hdpi/jog_tab_target_green.png b/res/drawable-hdpi/jog_tab_target_green.png
new file mode 100644
index 000000000..17f6b101e
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_target_green.png differ
diff --git a/res/drawable-hdpi/jog_tab_target_red.png b/res/drawable-hdpi/jog_tab_target_red.png
new file mode 100644
index 000000000..8db20bb63
Binary files /dev/null and b/res/drawable-hdpi/jog_tab_target_red.png differ
diff --git a/res/drawable/jog_tab_bar_left_answer.xml b/res/drawable/jog_tab_bar_left_answer.xml
new file mode 100644
index 000000000..32ce3dcda
--- /dev/null
+++ b/res/drawable/jog_tab_bar_left_answer.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/drawable/jog_tab_bar_right_decline.xml b/res/drawable/jog_tab_bar_right_decline.xml
new file mode 100644
index 000000000..83183ac1b
--- /dev/null
+++ b/res/drawable/jog_tab_bar_right_decline.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/drawable/jog_tab_left_answer.xml b/res/drawable/jog_tab_left_answer.xml
new file mode 100644
index 000000000..18ec7fa15
--- /dev/null
+++ b/res/drawable/jog_tab_left_answer.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/drawable/jog_tab_right_decline.xml b/res/drawable/jog_tab_right_decline.xml
new file mode 100644
index 000000000..a3bca5e92
--- /dev/null
+++ b/res/drawable/jog_tab_right_decline.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/incoming.xml b/res/layout/incoming.xml
index 1f3d8277f..401c254d0 100644
--- a/res/layout/incoming.xml
+++ b/res/layout/incoming.xml
@@ -4,7 +4,8 @@
android:layout_width="fill_parent" android:layout_height="fill_parent">
-
@@ -12,7 +13,8 @@
+ android:layout_below="@id/incoming_text"
+ android:layout_centerHorizontal="true" android:paddingTop="30dip">
-
-
-
-
+
diff --git a/res/values/slidingtab_style.xml b/res/values/slidingtab_style.xml
new file mode 100644
index 000000000..81c1693f6
--- /dev/null
+++ b/res/values/slidingtab_style.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/org/linphone/IncomingCallActivity.java b/src/org/linphone/IncomingCallActivity.java
index 540302b83..e21727c60 100644
--- a/src/org/linphone/IncomingCallActivity.java
+++ b/src/org/linphone/IncomingCallActivity.java
@@ -24,6 +24,8 @@ import org.linphone.core.LinphoneAddress;
import org.linphone.core.LinphoneCall;
import org.linphone.core.Log;
import org.linphone.core.LinphoneCall.State;
+import org.linphone.ui.SlidingTab;
+import org.linphone.ui.SlidingTab.OnTriggerListener;
import android.app.Activity;
import android.content.Intent;
@@ -32,7 +34,6 @@ import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
-import android.view.View.OnClickListener;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
@@ -43,13 +44,14 @@ import android.widget.Toast;
*
* @author Guillaume Beraudo
*/
-public class IncomingCallActivity extends Activity implements OnClickListener, LinphoneManagerReadyListener, LinphoneOnCallStateChangedListener {
+public class IncomingCallActivity extends Activity implements LinphoneManagerReadyListener, LinphoneOnCallStateChangedListener, OnTriggerListener {
private TextView mNameView;
private TextView mNumberView;
private ImageView mPictureView;
private LinphoneCall mCall;
- private LinphoneManagerWaitHelper helper;
+ private LinphoneManagerWaitHelper mHelper;
+ private SlidingTab mIncomingCallWidget;
private void findIncomingCall(Intent intent) {
String stringUri = intent.getStringExtra("stringUri");
@@ -68,15 +70,24 @@ public class IncomingCallActivity extends Activity implements OnClickListener, L
mNumberView = (TextView) findViewById(R.id.incoming_caller_number);
mPictureView = (ImageView) findViewById(R.id.incoming_picture);
- findViewById(R.id.Decline).setOnClickListener(this);
- findViewById(R.id.Answer).setOnClickListener(this);
-
// set this flag so this activity will stay in front of the keyguard
int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
flags |= WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
getWindow().addFlags(flags);
- helper = new LinphoneManagerWaitHelper(this, this);
+
+ // "Dial-to-answer" widget for incoming calls.
+ mIncomingCallWidget = (SlidingTab) findViewById(R.id.sliding_widget);
+
+ // For now, we only need to show two states: answer and decline.
+ // TODO: make left hint work
+// mIncomingCallWidget.setLeftHintText(R.string.slide_to_answer_hint);
+// mIncomingCallWidget.setRightHintText(R.string.slide_to_decline_hint);
+
+ mIncomingCallWidget.setOnTriggerListener(this);
+
+
+ mHelper = new LinphoneManagerWaitHelper(this, this);
super.onCreate(savedInstanceState);
}
@@ -108,7 +119,7 @@ public class IncomingCallActivity extends Activity implements OnClickListener, L
@Override
protected void onResume() {
super.onResume();
- helper.doManagerDependentOnResume();
+ mHelper.doManagerDependentOnResume();
}
@Override
@@ -126,24 +137,6 @@ public class IncomingCallActivity extends Activity implements OnClickListener, L
return super.onKeyDown(keyCode, event);
}
- @Override
- public void onClick(View v) {
- switch (v.getId()) {
- case R.id.Answer:
- if (!LinphoneManager.getInstance().acceptCall(mCall)) {
- // the above method takes care of Samsung Galaxy S
- Toast.makeText(this, R.string.couldnt_accept_call, Toast.LENGTH_LONG);
- }
- break;
- case R.id.Decline:
- LinphoneManager.getLc().terminateCall(mCall);
- break;
- default:
- throw new RuntimeException();
- }
- finish();
- }
-
@Override
public void onCallStateChanged(LinphoneCall call, State state, String msg) {
if (call == mCall && State.CallEnd == state) {
@@ -151,4 +144,33 @@ public class IncomingCallActivity extends Activity implements OnClickListener, L
}
}
+ private void decline() {
+ LinphoneManager.getLc().terminateCall(mCall);
+ }
+ private void answer() {
+ if (!LinphoneManager.getInstance().acceptCall(mCall)) {
+ // the above method takes care of Samsung Galaxy S
+ Toast.makeText(this, R.string.couldnt_accept_call, Toast.LENGTH_LONG);
+ }
+ }
+ @Override
+ public void onGrabbedStateChange(View v, int grabbedState) {
+ }
+
+ @Override
+ public void onTrigger(View v, int whichHandle) {
+ switch (whichHandle) {
+ case OnTriggerListener.LEFT_HANDLE:
+ answer();
+ finish();
+ break;
+ case OnTriggerListener.RIGHT_HANDLE:
+ decline();
+ finish();
+ break;
+ default:
+ break;
+ }
+ }
+
}
diff --git a/src/org/linphone/ui/SlidingTab.java b/src/org/linphone/ui/SlidingTab.java
new file mode 100644
index 000000000..c58670e39
--- /dev/null
+++ b/src/org/linphone/ui/SlidingTab.java
@@ -0,0 +1,844 @@
+/*
+ * Derived from Android "SlidingTab" source.
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.linphone.ui;
+
+
+import org.linphone.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Vibrator;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.TranslateAnimation;
+import android.view.animation.Animation.AnimationListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ImageView.ScaleType;
+
+/**
+ * A special widget containing two Sliders and a threshold for each. Moving either slider beyond
+ * the threshold will cause the registered OnTriggerListener.onTrigger() to be called with
+ * whichHandle being {@link OnTriggerListener#LEFT_HANDLE} or {@link OnTriggerListener#RIGHT_HANDLE}
+ * Equivalently, selecting a tab will result in a call to
+ * {@link OnTriggerListener#onGrabbedStateChange(View, int)} with one of these two states. Releasing
+ * the tab will result in whichHandle being {@link OnTriggerListener#NO_HANDLE}.
+ *
+ */
+public class SlidingTab extends ViewGroup {
+ private static final String LOG_TAG = "SlidingTab";
+ private static final int HORIZONTAL = 0; // as defined in attrs.xml
+ private static final int VERTICAL = 1;
+
+ // TODO: Make these configurable
+ private static final float THRESHOLD = 2.0f / 3.0f;
+ private static final long VIBRATE_SHORT = 30;
+ private static final long VIBRATE_LONG = 40;
+ private static final int TRACKING_MARGIN = 50;
+ private static final int ANIM_DURATION = 250; // Time for most animations (in ms)
+ private static final int ANIM_TARGET_TIME = 500; // Time to show targets (in ms)
+ private boolean mHoldLeftOnTransition = false;
+ private boolean mHoldRightOnTransition = false;
+
+ private OnTriggerListener mOnTriggerListener;
+ private int mGrabbedState = OnTriggerListener.NO_HANDLE;
+ private boolean mTriggered = false;
+ private Vibrator mVibrator;
+
+ /**
+ * Either {@link #HORIZONTAL} or {@link #VERTICAL}.
+ */
+ private int mOrientation;
+
+ private Slider mLeftSlider;
+ private Slider mRightSlider;
+ private Slider mCurrentSlider;
+ private boolean mTracking;
+ private float mThreshold;
+ private Slider mOtherSlider;
+ private boolean mAnimating;
+ private Rect mTmpRect;
+
+ /**
+ * Listener used to reset the view when the current animation completes.
+ */
+ private final AnimationListener mAnimationDoneListener = new AnimationListener() {
+ public void onAnimationStart(Animation animation) {
+
+ }
+
+ public void onAnimationRepeat(Animation animation) {
+
+ }
+
+ public void onAnimationEnd(Animation animation) {
+ onAnimationDone();
+ }
+ };
+
+ /**
+ * Interface definition for a callback to be invoked when a tab is triggered
+ * by moving it beyond a threshold.
+ */
+ public interface OnTriggerListener {
+ /**
+ * The interface was triggered because the user let go of the handle without reaching the
+ * threshold.
+ */
+ public static final int NO_HANDLE = 0;
+
+ /**
+ * The interface was triggered because the user grabbed the left handle and moved it past
+ * the threshold.
+ */
+ public static final int LEFT_HANDLE = 1;
+
+ /**
+ * The interface was triggered because the user grabbed the right handle and moved it past
+ * the threshold.
+ */
+ public static final int RIGHT_HANDLE = 2;
+
+ /**
+ * Called when the user moves a handle beyond the threshold.
+ *
+ * @param v The view that was triggered.
+ * @param whichHandle Which "dial handle" the user grabbed,
+ * either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}.
+ */
+ void onTrigger(View v, int whichHandle);
+
+ /**
+ * Called when the "grabbed state" changes (i.e. when the user either grabs or releases
+ * one of the handles.)
+ *
+ * @param v the view that was triggered
+ * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #LEFT_HANDLE},
+ * or {@link #RIGHT_HANDLE}.
+ */
+ void onGrabbedStateChange(View v, int grabbedState);
+ }
+
+ /**
+ * Simple container class for all things pertinent to a slider.
+ * A slider consists of 3 Views:
+ *
+ * {@link #tab} is the tab shown on the screen in the default state.
+ * {@link #text} is the view revealed as the user slides the tab out.
+ * {@link #target} is the target the user must drag the slider past to trigger the slider.
+ *
+ */
+ private static class Slider {
+ /**
+ * Tab alignment - determines which side the tab should be drawn on
+ */
+ public static final int ALIGN_LEFT = 0;
+ public static final int ALIGN_RIGHT = 1;
+ public static final int ALIGN_TOP = 2;
+ public static final int ALIGN_BOTTOM = 3;
+ public static final int ALIGN_UNKNOWN = 4;
+
+ /**
+ * States for the view.
+ */
+ private static final int STATE_NORMAL = 0;
+ private static final int STATE_PRESSED = 1;
+ private static final int STATE_ACTIVE = 2;
+
+ private final ImageView tab;
+ private final TextView text;
+ private final ImageView target;
+ private int currentState = STATE_NORMAL;
+ private int alignment = ALIGN_UNKNOWN;
+ private int alignment_value;
+
+ /**
+ * Constructor
+ *
+ * @param parent the container view of this one
+ * @param tabId drawable for the tab
+ * @param barId drawable for the bar
+ * @param targetId drawable for the target
+ */
+ Slider(ViewGroup parent, int iconId, int tabId, int barId, int targetId) {
+ // Create tab
+ tab = new ImageView(parent.getContext());
+ tab.setImageResource(iconId);
+ tab.setBackgroundResource(tabId);
+ tab.setScaleType(ScaleType.CENTER);
+ tab.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT));
+
+ // Create hint TextView
+ text = new TextView(parent.getContext());
+ text.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.FILL_PARENT));
+ text.setBackgroundResource(barId);
+ text.setTextAppearance(parent.getContext(), R.style.TextAppearance_SlidingTabNormal);
+ // hint.setSingleLine(); // Hmm.. this causes the text to disappear off-screen
+
+ // Create target
+ target = new ImageView(parent.getContext());
+ target.setImageResource(targetId);
+ target.setScaleType(ScaleType.CENTER);
+ target.setLayoutParams(
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+ target.setVisibility(View.INVISIBLE);
+
+ parent.addView(target); // this needs to be first - relies on painter's algorithm
+ parent.addView(tab);
+ parent.addView(text);
+ }
+
+ void setIcon(int iconId) {
+ tab.setImageResource(iconId);
+ }
+
+ void setTabBackgroundResource(int tabId) {
+ tab.setBackgroundResource(tabId);
+ }
+
+ void setBarBackgroundResource(int barId) {
+ text.setBackgroundResource(barId);
+ }
+
+ void setHintText(int resId) {
+ text.setText(resId);
+ }
+
+ void hide() {
+ boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
+ int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getRight()
+ : alignment_value - tab.getLeft()) : 0;
+ int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getBottom()
+ : alignment_value - tab.getTop());
+
+ Animation trans = new TranslateAnimation(0, dx, 0, dy);
+ trans.setDuration(ANIM_DURATION);
+ trans.setFillAfter(true);
+ tab.startAnimation(trans);
+ text.startAnimation(trans);
+ target.setVisibility(View.INVISIBLE);
+ }
+
+ void show(boolean animate) {
+ text.setVisibility(View.VISIBLE);
+ tab.setVisibility(View.VISIBLE);
+ //target.setVisibility(View.INVISIBLE);
+ if (animate) {
+ boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
+ int dx = horiz ? (alignment == ALIGN_LEFT ? tab.getWidth() : -tab.getWidth()) : 0;
+ int dy = horiz ? 0: (alignment == ALIGN_TOP ? tab.getHeight() : -tab.getHeight());
+
+ Animation trans = new TranslateAnimation(-dx, 0, -dy, 0);
+ trans.setDuration(ANIM_DURATION);
+ tab.startAnimation(trans);
+ text.startAnimation(trans);
+ }
+ }
+
+ void setState(int state) {
+ text.setPressed(state == STATE_PRESSED);
+ tab.setPressed(state == STATE_PRESSED);
+ if (state == STATE_ACTIVE) {
+ final int[] activeState = new int[] {android.R.attr.state_active};
+ if (text.getBackground().isStateful()) {
+ text.getBackground().setState(activeState);
+ }
+ if (tab.getBackground().isStateful()) {
+ tab.getBackground().setState(activeState);
+ }
+ text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabActive);
+ } else {
+ text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
+ }
+ currentState = state;
+ }
+
+ void showTarget() {
+ AlphaAnimation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
+ alphaAnim.setDuration(ANIM_TARGET_TIME);
+ target.startAnimation(alphaAnim);
+ target.setVisibility(View.VISIBLE);
+ }
+
+ void reset(boolean animate) {
+ setState(STATE_NORMAL);
+ text.setVisibility(View.VISIBLE);
+ text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
+ tab.setVisibility(View.VISIBLE);
+ target.setVisibility(View.INVISIBLE);
+ final boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
+ int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getLeft()
+ : alignment_value - tab.getRight()) : 0;
+ int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getTop()
+ : alignment_value - tab.getBottom());
+ if (animate) {
+ TranslateAnimation trans = new TranslateAnimation(0, dx, 0, dy);
+ trans.setDuration(ANIM_DURATION);
+ trans.setFillAfter(false);
+ text.startAnimation(trans);
+ tab.startAnimation(trans);
+ } else {
+ if (horiz) {
+ text.offsetLeftAndRight(dx);
+ tab.offsetLeftAndRight(dx);
+ } else {
+ text.offsetTopAndBottom(dy);
+ tab.offsetTopAndBottom(dy);
+ }
+ text.clearAnimation();
+ tab.clearAnimation();
+ target.clearAnimation();
+ }
+ }
+
+ void setTarget(int targetId) {
+ target.setImageResource(targetId);
+ }
+
+ /**
+ * Layout the given widgets within the parent.
+ *
+ * @param l the parent's left border
+ * @param t the parent's top border
+ * @param r the parent's right border
+ * @param b the parent's bottom border
+ * @param alignment which side to align the widget to
+ */
+ void layout(int l, int t, int r, int b, int alignment) {
+ this.alignment = alignment;
+ final Drawable tabBackground = tab.getBackground();
+ final int handleWidth = tabBackground.getIntrinsicWidth();
+ final int handleHeight = tabBackground.getIntrinsicHeight();
+ final Drawable targetDrawable = target.getDrawable();
+ final int targetWidth = targetDrawable.getIntrinsicWidth();
+ final int targetHeight = targetDrawable.getIntrinsicHeight();
+ final int parentWidth = r - l;
+ final int parentHeight = b - t;
+
+ final int leftTarget = (int) (THRESHOLD * parentWidth) - targetWidth + handleWidth / 2;
+ final int rightTarget = (int) ((1.0f - THRESHOLD) * parentWidth) - handleWidth / 2;
+ final int left = (parentWidth - handleWidth) / 2;
+ final int right = left + handleWidth;
+
+ if (alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT) {
+ // horizontal
+ final int targetTop = (parentHeight - targetHeight) / 2;
+ final int targetBottom = targetTop + targetHeight;
+ final int top = (parentHeight - handleHeight) / 2;
+ final int bottom = (parentHeight + handleHeight) / 2;
+ if (alignment == ALIGN_LEFT) {
+ tab.layout(0, top, handleWidth, bottom);
+ text.layout(0 - parentWidth, top, 0, bottom);
+ text.setGravity(Gravity.RIGHT);
+ target.layout(leftTarget, targetTop, leftTarget + targetWidth, targetBottom);
+ alignment_value = l;
+ } else {
+ tab.layout(parentWidth - handleWidth, top, parentWidth, bottom);
+ text.layout(parentWidth, top, parentWidth + parentWidth, bottom);
+ target.layout(rightTarget, targetTop, rightTarget + targetWidth, targetBottom);
+ text.setGravity(Gravity.TOP);
+ alignment_value = r;
+ }
+ } else {
+ // vertical
+ final int targetLeft = (parentWidth - targetWidth) / 2;
+ final int targetRight = (parentWidth + targetWidth) / 2;
+ final int top = (int) (THRESHOLD * parentHeight) + handleHeight / 2 - targetHeight;
+ final int bottom = (int) ((1.0f - THRESHOLD) * parentHeight) - handleHeight / 2;
+ if (alignment == ALIGN_TOP) {
+ tab.layout(left, 0, right, handleHeight);
+ text.layout(left, 0 - parentHeight, right, 0);
+ target.layout(targetLeft, top, targetRight, top + targetHeight);
+ alignment_value = t;
+ } else {
+ tab.layout(left, parentHeight - handleHeight, right, parentHeight);
+ text.layout(left, parentHeight, right, parentHeight + parentHeight);
+ target.layout(targetLeft, bottom, targetRight, bottom + targetHeight);
+ alignment_value = b;
+ }
+ }
+ }
+
+ public void updateDrawableStates() {
+ setState(currentState);
+ }
+
+ /**
+ * Ensure all the dependent widgets are measured.
+ */
+ public void measure() {
+ tab.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
+ View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
+ text.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
+ View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
+ }
+
+ /**
+ * Get the measured tab width. Must be called after {@link Slider#measure()}.
+ * @return
+ */
+ public int getTabWidth() {
+ return tab.getMeasuredWidth();
+ }
+
+ /**
+ * Get the measured tab width. Must be called after {@link Slider#measure()}.
+ * @return
+ */
+ public int getTabHeight() {
+ return tab.getMeasuredHeight();
+ }
+
+ /**
+ * Start animating the slider. Note we need two animations since an Animator
+ * keeps internal state of the invalidation region which is just the view being animated.
+ *
+ * @param anim1
+ * @param anim2
+ */
+ public void startAnimation(Animation anim1, Animation anim2) {
+ tab.startAnimation(anim1);
+ text.startAnimation(anim2);
+ }
+
+ public void hideTarget() {
+ target.clearAnimation();
+ target.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ public SlidingTab(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Constructor used when this widget is created from a layout file.
+ */
+ public SlidingTab(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // Allocate a temporary once that can be used everywhere.
+ mTmpRect = new Rect();
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab);
+ mOrientation = a.getInt(org.linphone.R.styleable.SlidingTab_orientation, HORIZONTAL);
+ a.recycle();
+ mLeftSlider = new Slider(this,
+ R.drawable.startcall_green,
+ R.drawable.jog_tab_left_answer,
+ R.drawable.jog_tab_bar_left_answer,
+ R.drawable.jog_tab_target_green
+ );
+ mRightSlider = new Slider(this,
+ R.drawable.stopcall_red,
+ R.drawable.jog_tab_right_decline,
+ R.drawable.jog_tab_bar_right_decline,
+ R.drawable.jog_tab_target_red
+ );
+
+ // setBackgroundColor(0x80808080);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+
+ int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
+ Log.e("SlidingTab", "SlidingTab cannot have UNSPECIFIED MeasureSpec"
+ +"(wspec=" + widthSpecMode + ", hspec=" + heightSpecMode + ")",
+ new RuntimeException(LOG_TAG + "stack:"));
+ }
+
+ mLeftSlider.measure();
+ mRightSlider.measure();
+ final int leftTabWidth = mLeftSlider.getTabWidth();
+ final int rightTabWidth = mRightSlider.getTabWidth();
+ final int leftTabHeight = mLeftSlider.getTabHeight();
+ final int rightTabHeight = mRightSlider.getTabHeight();
+ final int width;
+ final int height;
+ if (isHorizontal()) {
+ width = Math.max(widthSpecSize, leftTabWidth + rightTabWidth);
+ height = Math.max(leftTabHeight, rightTabHeight);
+ } else {
+ width = Math.max(leftTabWidth, rightTabHeight);
+ height = Math.max(heightSpecSize, leftTabHeight + rightTabHeight);
+ }
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ final int action = event.getAction();
+ final float x = event.getX();
+ final float y = event.getY();
+
+ if (mAnimating) {
+ return false;
+ }
+
+ View leftHandle = mLeftSlider.tab;
+ leftHandle.getHitRect(mTmpRect);
+ boolean leftHit = mTmpRect.contains((int) x, (int) y);
+
+ View rightHandle = mRightSlider.tab;
+ rightHandle.getHitRect(mTmpRect);
+ boolean rightHit = mTmpRect.contains((int)x, (int) y);
+
+ if (!mTracking && !(leftHit || rightHit)) {
+ return false;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ mTracking = true;
+ mTriggered = false;
+ vibrate(VIBRATE_SHORT);
+ if (leftHit) {
+ mCurrentSlider = mLeftSlider;
+ mOtherSlider = mRightSlider;
+ mThreshold = isHorizontal() ? THRESHOLD : 1.0f - THRESHOLD;
+ setGrabbedState(OnTriggerListener.LEFT_HANDLE);
+ } else {
+ mCurrentSlider = mRightSlider;
+ mOtherSlider = mLeftSlider;
+ mThreshold = isHorizontal() ? 1.0f - THRESHOLD : THRESHOLD;
+ setGrabbedState(OnTriggerListener.RIGHT_HANDLE);
+ }
+ mCurrentSlider.setState(Slider.STATE_PRESSED);
+ mCurrentSlider.showTarget();
+ mOtherSlider.hide();
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Reset the tabs to their original state and stop any existing animation.
+ * Animate them back into place if animate is true.
+ *
+ * @param animate
+ */
+ public void reset(boolean animate) {
+ mLeftSlider.reset(animate);
+ mRightSlider.reset(animate);
+ if (!animate) {
+ mAnimating = false;
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ // Clear animations so sliders don't continue to animate when we show the widget again.
+ if (visibility != getVisibility() && visibility == View.INVISIBLE) {
+ reset(false);
+ }
+ super.setVisibility(visibility);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mTracking) {
+ final int action = event.getAction();
+ final float x = event.getX();
+ final float y = event.getY();
+
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ if (withinView(x, y, this) ) {
+ moveHandle(x, y);
+ float position = isHorizontal() ? x : y;
+ float target = mThreshold * (isHorizontal() ? getWidth() : getHeight());
+ boolean thresholdReached;
+ if (isHorizontal()) {
+ thresholdReached = mCurrentSlider == mLeftSlider ?
+ position > target : position < target;
+ } else {
+ thresholdReached = mCurrentSlider == mLeftSlider ?
+ position < target : position > target;
+ }
+ if (!mTriggered && thresholdReached) {
+ mTriggered = true;
+ mTracking = false;
+ mCurrentSlider.setState(Slider.STATE_ACTIVE);
+ boolean isLeft = mCurrentSlider == mLeftSlider;
+ dispatchTriggerEvent(isLeft ?
+ OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE);
+
+ startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition);
+ setGrabbedState(OnTriggerListener.NO_HANDLE);
+ }
+ break;
+ }
+ // Intentionally fall through - we're outside tracking rectangle
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mTracking = false;
+ mTriggered = false;
+ mOtherSlider.show(true);
+ mCurrentSlider.reset(false);
+ mCurrentSlider.hideTarget();
+ mCurrentSlider = null;
+ mOtherSlider = null;
+ setGrabbedState(OnTriggerListener.NO_HANDLE);
+ break;
+ }
+ }
+
+ return mTracking || super.onTouchEvent(event);
+ }
+
+ void startAnimating(final boolean holdAfter) {
+ mAnimating = true;
+ final Animation trans1;
+ final Animation trans2;
+ final Slider slider = mCurrentSlider;
+ final int dx;
+ final int dy;
+ if (isHorizontal()) {
+ int right = slider.tab.getRight();
+ int width = slider.tab.getWidth();
+ int left = slider.tab.getLeft();
+ int viewWidth = getWidth();
+ int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim
+ dx = slider == mRightSlider ? - (right + viewWidth - holdOffset)
+ : (viewWidth - left) + viewWidth - holdOffset;
+ dy = 0;
+ } else {
+ int top = slider.tab.getTop();
+ int bottom = slider.tab.getBottom();
+ int height = slider.tab.getHeight();
+ int viewHeight = getHeight();
+ int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim
+ dx = 0;
+ dy = slider == mRightSlider ? (top + viewHeight - holdOffset)
+ : - ((viewHeight - bottom) + viewHeight - holdOffset);
+ }
+ trans1 = new TranslateAnimation(0, dx, 0, dy);
+ trans1.setDuration(ANIM_DURATION);
+ trans1.setInterpolator(new LinearInterpolator());
+ trans1.setFillAfter(true);
+ trans2 = new TranslateAnimation(0, dx, 0, dy);
+ trans2.setDuration(ANIM_DURATION);
+ trans2.setInterpolator(new LinearInterpolator());
+ trans2.setFillAfter(true);
+
+ trans1.setAnimationListener(new AnimationListener() {
+ public void onAnimationEnd(Animation animation) {
+ Animation anim;
+ if (holdAfter) {
+ anim = new TranslateAnimation(dx, dx, dy, dy);
+ anim.setDuration(1000); // plenty of time for transitions
+ mAnimating = false;
+ } else {
+ anim = new AlphaAnimation(0.5f, 1.0f);
+ anim.setDuration(ANIM_DURATION);
+ resetView();
+ }
+ anim.setAnimationListener(mAnimationDoneListener);
+
+ /* Animation can be the same for these since the animation just holds */
+ mLeftSlider.startAnimation(anim, anim);
+ mRightSlider.startAnimation(anim, anim);
+ }
+
+ public void onAnimationRepeat(Animation animation) {
+
+ }
+
+ public void onAnimationStart(Animation animation) {
+
+ }
+
+ });
+
+ slider.hideTarget();
+ slider.startAnimation(trans1, trans2);
+ }
+
+ private void onAnimationDone() {
+ resetView();
+ mAnimating = false;
+ }
+
+ private boolean withinView(final float x, final float y, final View view) {
+ return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight()
+ || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth();
+ }
+
+ private boolean isHorizontal() {
+ return mOrientation == HORIZONTAL;
+ }
+
+ private void resetView() {
+ mLeftSlider.reset(false);
+ mRightSlider.reset(false);
+ // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight());
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ if (!changed) return;
+
+ // Center the widgets in the view
+ mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM);
+ mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP);
+ }
+
+ private void moveHandle(float x, float y) {
+ final View handle = mCurrentSlider.tab;
+ final View content = mCurrentSlider.text;
+ if (isHorizontal()) {
+ int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2);
+ handle.offsetLeftAndRight(deltaX);
+ content.offsetLeftAndRight(deltaX);
+ } else {
+ int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2);
+ handle.offsetTopAndBottom(deltaY);
+ content.offsetTopAndBottom(deltaY);
+ }
+ invalidate(); // TODO: be more conservative about what we're invalidating
+ }
+
+ /**
+ * Sets the left handle icon to a given resource.
+ *
+ * The resource should refer to a Drawable object, or use 0 to remove
+ * the icon.
+ *
+ * @param iconId the resource ID of the icon drawable
+ * @param targetId the resource of the target drawable
+ * @param barId the resource of the bar drawable (stateful)
+ * @param tabId the resource of the
+ */
+ public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) {
+ mLeftSlider.setIcon(iconId);
+ mLeftSlider.setTarget(targetId);
+ mLeftSlider.setBarBackgroundResource(barId);
+ mLeftSlider.setTabBackgroundResource(tabId);
+ mLeftSlider.updateDrawableStates();
+ }
+
+ /**
+ * Sets the left handle hint text to a given resource string.
+ *
+ * @param resId
+ */
+ public void setLeftHintText(int resId) {
+ if (isHorizontal()) {
+ mLeftSlider.setHintText(resId);
+ }
+ }
+
+ /**
+ * Sets the right handle icon to a given resource.
+ *
+ * The resource should refer to a Drawable object, or use 0 to remove
+ * the icon.
+ *
+ * @param iconId the resource ID of the icon drawable
+ * @param targetId the resource of the target drawable
+ * @param barId the resource of the bar drawable (stateful)
+ * @param tabId the resource of the
+ */
+ public void setRightTabResources(int iconId, int targetId, int barId, int tabId) {
+ mRightSlider.setIcon(iconId);
+ mRightSlider.setTarget(targetId);
+ mRightSlider.setBarBackgroundResource(barId);
+ mRightSlider.setTabBackgroundResource(tabId);
+ mRightSlider.updateDrawableStates();
+ }
+
+ /**
+ * Sets the left handle hint text to a given resource string.
+ *
+ * @param resId
+ */
+ public void setRightHintText(int resId) {
+ if (isHorizontal()) {
+ mRightSlider.setHintText(resId);
+ }
+ }
+
+ public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) {
+ mHoldLeftOnTransition = holdLeft;
+ mHoldRightOnTransition = holdRight;
+ }
+
+ /**
+ * Triggers haptic feedback.
+ */
+ private synchronized void vibrate(long duration) {
+ if (mVibrator == null) {
+ mVibrator = (android.os.Vibrator)
+ getContext().getSystemService(Context.VIBRATOR_SERVICE);
+ }
+ mVibrator.vibrate(duration);
+ }
+
+ /**
+ * Registers a callback to be invoked when the user triggers an event.
+ *
+ * @param listener the OnDialTriggerListener to attach to this view
+ */
+ public void setOnTriggerListener(OnTriggerListener listener) {
+ mOnTriggerListener = listener;
+ }
+
+ /**
+ * Dispatches a trigger event to listener. Ignored if a listener is not set.
+ * @param whichHandle the handle that triggered the event.
+ */
+ private void dispatchTriggerEvent(int whichHandle) {
+ vibrate(VIBRATE_LONG);
+ if (mOnTriggerListener != null) {
+ mOnTriggerListener.onTrigger(this, whichHandle);
+ }
+ }
+
+ /**
+ * Sets the current grabbed state, and dispatches a grabbed state change
+ * event to our listener.
+ */
+ private void setGrabbedState(int newState) {
+ if (newState != mGrabbedState) {
+ mGrabbedState = newState;
+ if (mOnTriggerListener != null) {
+ mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
+ }
+ }
+ }
+}