Use Android SlidingTab for incoming call activity.

This commit is contained in:
Guillaume Beraudo 2011-10-11 11:38:53 +02:00
parent 7f2efdb451
commit f795954e9a
22 changed files with 1046 additions and 43 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- StateListDrawable used for buttons in the in-call onscreen touch UI. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/jog_tab_bar_left_end_pressed" />
<item android:state_enabled="true"
android:drawable="@drawable/jog_tab_bar_left_end_normal" />
<item android:state_active="true"
android:drawable="@drawable/jog_tab_bar_left_end_confirm_green" />
</selector>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- StateListDrawable used for buttons in the in-call onscreen touch UI. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/jog_tab_bar_right_end_pressed" />
<item android:state_enabled="true"
android:drawable="@drawable/jog_tab_bar_right_end_normal" />
<item android:state_active="true"
android:drawable="@drawable/jog_tab_bar_right_end_confirm_red" />
</selector>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- StateListDrawable used for buttons in the in-call onscreen touch UI. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/jog_tab_left_pressed" />
<item android:state_enabled="true"
android:drawable="@drawable/jog_tab_left_normal" />
<item android:state_active="true"
android:drawable="@drawable/jog_tab_left_confirm_green" />
</selector>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- StateListDrawable used for buttons in the in-call onscreen touch UI. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/jog_tab_right_pressed" />
<item android:state_enabled="true"
android:drawable="@drawable/jog_tab_right_normal" />
<item android:state_active="true"
android:drawable="@drawable/jog_tab_right_confirm_red" />
</selector>

View file

@ -4,7 +4,8 @@
android:layout_width="fill_parent" android:layout_height="fill_parent">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
<TextView android:id="@+id/incoming_text"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="25sp" android:layout_alignParentTop="true"
android:text="Incoming call" android:textAppearance="?android:attr/textAppearanceLarge">
@ -12,7 +13,8 @@
<LinearLayout android:layout_width="wrap_content"
android:layout_height="wrap_content" android:orientation="vertical"
android:layout_centerInParent="true">
android:layout_below="@id/incoming_text"
android:layout_centerHorizontal="true" android:paddingTop="30dip">
<ImageView android:id="@+id/incoming_picture"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:layout_gravity="center" android:scaleType="fitCenter"
@ -33,21 +35,13 @@
</LinearLayout>
<ImageButton android:id="@+id/Answer"
android:src="@drawable/startcall_green" android:layout_width="wrap_content"
<org.linphone.ui.SlidingTab
android:id="@+id/sliding_widget"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="answer"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_alignParentLeft="true" android:layout_marginBottom="40sp"
android:layout_alignParentBottom="true"/>
<ImageButton android:id="@+id/Decline"
android:src="@drawable/stopcall_red" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="decline"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_alignParentRight="true" android:layout_alignBottom="@id/Answer"/>
android:layout_alignParentBottom="true"
android:layout_marginBottom="80dip"
/>
<!-- <org.linphone.ui.CallButton android:src="@drawable/startvideo_green" android:layout_width="wrap_content"-->

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="TextAppearance.SlidingTabNormal"
parent="@android:attr/textAppearanceMedium">
<item name="android:textColor">?android:attr/textColorTertiary</item>
<item name="android:textSize">28sp</item>
<item name="android:shadowColor">@android:color/black</item>
<item name="android:shadowDx">0.0</item>
<item name="android:shadowDy">1.0</item>
<item name="android:shadowRadius">5.0</item>
</style>
<style name="TextAppearance.SlidingTabActive"
parent="@android:attr/textAppearanceMedium">
<item name="android:textColor">@android:color/black</item>
<item name="android:textSize">28sp</item>
</style>
<attr name="orientation">
<!-- Defines an horizontal widget. -->
<enum name="horizontal" value="0" />
<!-- Defines a vertical widget. -->
<enum name="vertical" value="1" />
</attr>
<declare-styleable name="SlidingTab">
<!-- Use "horizontal" for a row, "vertical" for a column. The default is horizontal. -->
<attr name="orientation" />
</declare-styleable>
</resources>

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}
}