您的位置:首页 > 产品设计 > UI/UE

通话中UI结构分析

2017-03-14 11:40 639 查看

InCallActivity

通话UI就是packages/apps/InCallUI/src/com/android/incallui/InCallActivity.java

代码在InCallUI中不过AndroidManifest中的定义却在packages/apps/Dialer/AndroidManifest.xml,这个是因为InCallUI和Dialer目前编译成一个apk,详细可见Android.mk。

<activity android:name="com.android.incallui.InCallActivity"
android:theme="@style/Theme.InCallScreen"
android:label="@string/phoneAppLabel"
android:excludeFromRecents="true"
android:launchMode="singleInstance"
android:configChanges="keyboardHidden|keyboard|navigation|mnc|mcc"
android:exported="false"
android:screenOrientation="nosensor" >
从xml中可见InCallActivity不在activity历史中显示;export=false,这样其它进程发送intent对其是无效的;启动模式是singleInstance,这样内存中是在单独的一个Activity Stack中,且该Stack中只有一个实例。

下面以拨号为例看下UI是怎么启动的:

@Override
protected void onCreate(Bundle icicle) {
...
setContentView(R.layout.incall_screen);
...
internalResolveIntent(getIntent());
...
}
incall_screen布局文件:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main" >
</FrameLayout>
布局文件是空的,和Android5.0之前不一样了

private void internalResolveIntent(Intent intent) {
...
if (intent.getBooleanExtra(NEW_OUTGOING_CALL_EXTRA, false)) {
...
Point touchPoint = null;
if (TouchPointManager.getInstance().hasValidPoint()) {
// Use the most immediate touch point in the InCallUi if available
touchPoint = TouchPointManager.getInstance().getPoint();
} else {
// Otherwise retrieve the touch point from the call intent
if (call != null) {
touchPoint = (Point) extras.getParcelable(TouchPointManager.TOUCH_POINT);
}
}
CircularRevealFragment.startCircularReveal(getFragmentManager(), touchPoint,
InCallPresenter.getInstance());
...
}
...
}
如果是拨号,那么代码走到packages/apps/InCallUI/src/com/android/incallui/CircularRevealFragment.java,该类纯粹是为了UI效果。它实现了从点击点开始圆形扩散到全屏的动画,然后显示通话UI,touchPoint的作用是传递点击点的位置。这个动画不是为了动画而动画,其实是Android5.0重构后通话UI显示很慢(我测试所有fragment加载完毕大概要500ms),因为首先是拨号走的流程复杂了很多,其次是不再像5.0之前那样InCallScreen一直在内存中。加入这个动画掩盖了UI加载慢的问题。

public static void startCircularReveal(FragmentManager fm, Point touchPoint,
OnCircularRevealCompleteListener listener) {
Log.v(TAG, "[startCircularReveal]prepare animation");
if (fm.findFragmentByTag(TAG) == null) {
fm.beginTransaction().add(R.id.main,
new CircularRevealFragment(touchPoint, listener), TAG)
.commitAllowingStateLoss();
} else {
Log.w(TAG, "An instance of CircularRevealFragment already exists");
}
}
添加fragment

@Override
public void onResume() {
super.onResume();
if (!mAnimationStarted) {
// Only run the animation once for each instance of the fragment
startOutgoingAnimation(InCallPresenter.getInstance().getThemeColors());
}
mAnimationStarted = true;
}
onResume中开始动画

public void startOutgoingAnimation(MaterialPalette palette) {
final Activity activity = getActivity();
final View view  = activity.getWindow().getDecorView();
...
view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
final ViewTreeObserver vto = view.getViewTreeObserver();
if (vto.isAlive()) {
vto.removeOnPreDrawListener(this);
}
final Animator animator = getRevealAnimator(mTouchPoint);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setClipToOutline(false);
if (mListener != null) {
mListener.onCircularRevealComplete(getFragmentManager());
}
}
});
Log.v(TAG, "[onPreDraw]animation triggered");
animator.start();
return false;
}
});
Log.v(TAG, "[startOutgoingAnimation]invalidate to force refresh");
view.invalidate();
}
OnPreDrawListener中启动动画,动画结束后调用onCircularRevealComplete,实现OnCircularRevealCompleteListener的是packages/apps/InCallUI/src/com/android/incallui/InCallPresenter.java

@Override
public void onCircularRevealComplete(FragmentManager fm) {
if (mInCallActivity != null) {
mInCallActivity.showCallCardFragment(true);
mInCallActivity.getCallCardFragment().animateForNewOutgoingCall();
CircularRevealFragment.endCircularReveal(mInCallActivity.getFragmentManager());
}
}
这里showCallCardFragment方法开始显示通话UI,animateForNewOutgoingCall开始callcard加载时的动画,endCircularReveal把CircularRevealFragment从UI中移除。

public void showCallCardFragment(boolean show) {
showFragment(TAG_CALLCARD_FRAGMENT, show, true);
}
private void showFragment(String tag, boolean show, boolean executeImmediately) {
final FragmentManager fm = getFragmentManagerForTag(tag);
Fragment fragment = fm.findFragmentByTag(tag);
...
final FragmentTransaction transaction = fm.beginTransaction();
if (show) {
if (fragment == null) {
fragment = createNewFragmentForTag(tag);
transaction.add(getContainerIdForFragment(tag), fragment, tag);
} else {
transaction.show(fragment);
}
} else {
transaction.hide(fragment);
}

transaction.commitAllowingStateLoss();
if (executeImmediately) {
fm.executePendingTransactions();
}
}
该方法使用FragmentManager添加或者隐藏fragment,这里show=true,fragment=null,所以会调用createNewFragmentForTag

private Fragment createNewFragmentForTag(String tag) {
...
} else if (TAG_CALLCARD_FRAGMENT.equals(tag)) {
mCallCardFragment = new CallCardFragment();
return mCallCardFragment;
}
throw new IllegalStateException("Unexpected fragment: " + tag);
}
createNewFragmentForTag就是新建了一个CallCardFragment,UI显示的大概流程到此结束。除了CallCardFragment外,还有其他几个Fragment:

private CallButtonFragment mCallButtonFragment;   //通话中按键
private CallCardFragment mCallCardFragment;      //显示联系人头像,名字等
private AnswerFragment mAnswerFragment;          //来电时显示,用于接听、拒绝电话
private DialpadFragment mDialpadFragment;        //dtmf拨号盘,例如10086时候用于键盘输入
private ConferenceManagerFragment mConferenceManagerFragment;   //gsm会议通话管理
各个Fragement在不同的时候加载,CallCardFragment是比较基础的显示部分,大部分时候它是常驻的Fragment

MVP模式

UI中设计模式使用了MVP模式,从文件名就能看出来,每个fragment都有对应的Presenter,例如callcard相关的两个类:

packages/apps/InCallUI/src/com/android/incallui/CallCardFragment.java

packages/apps/InCallUI/src/com/android/incallui/CallCardPresenter.java

public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPresenter.CallCardUi>
implements CallCardPresenter.CallCardUi
继承了BaseFragment类且实现了一个接口,这里先分析packages/apps/InCallUI/src/com/android/incallui/BaseFragment.java

public abstract class BaseFragment<T extends Presenter<U>, U extends Ui> extends Fragment
抽象类,这里的packages/apps/InCallUI/src/com/android/incallui/Presenter.java定义如下:

public abstract class Presenter<U extends Ui> {

private U mUi;

/**
* Called after the UI view has been created.  That is when fragment.onViewCreated() is called.
*
* @param ui The Ui implementation that is now ready to be used.
*/
public void onUiReady(U ui) {
mUi = ui;
}

/**
* Called when the UI view is destroyed in Fragment.onDestroyView().
*/
public final void onUiDestroy(U ui) {
onUiUnready(ui);
mUi = null;
}

/**
* To be overriden by Presenter implementations.  Called when the fragment is being
* destroyed but before ui is set to null.
*/
public void onUiUnready(U ui) {
}

public void onSaveInstanceState(Bundle outState) {}

public void onRestoreInstanceState(Bundle savedInstanceState) {}

public U getUi() {
return mUi;
}
}
还是个抽象类,能看出最重要的是有个packages/apps/InCallUI/src/com/android/incallui/Ui.java对象。

public interface Ui {

}
Ui是个接口没有任何方法。回到BaseFragment中:

private T mPresenter;

public abstract T createPresenter();

public abstract U getUi();

protected BaseFragment() {
mPresenter = createPresenter();
}

public T getPresenter() {
return mPresenter;
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mPresenter.onUiReady(getUi());
}

...

@Override
public void onDestroyView() {
super.onDestroyView();
mPresenter.onUiDestroy(getUi());
}
BaseFragment中有Presenter和Ui对象,并在actvity创建和销毁的时候把这两个对象绑定和分离,即Ui(虽然BaseFragment没有实现Ui,但是子类全部是实现Ui的)中通过createPresenter直接创建了Presenter,而Presenter中通过OnUiReady和onUiDestory获取和释放Ui。下面看CallCardPresenter

@Override
public void onUiReady(CallCardUi ui) {
super.onUiReady(ui);
...
InCallPresenter.getInstance().addListener(this);
InCallPresenter.getInstance().addIncomingCallListener(this);
...
}

@Override
public void onUiUnready(CallCardUi ui) {
super.onUiUnready(ui);

InCallPresenter.getInstance().removeListener(this);
InCallPresenter.getInstance().removeIncomingCallListener(this);
...

mPrimary = null;
mPrimaryContactInfo = null;
mSecondaryContactInfo = null;
}
重写了Presenter的两个方法,这两个方法中注册或者卸载对事件的监听,事件有来电、通话状态变化等等。这两个方法是在activity创建和销毁时调用的,即等同于在activity创建和销毁时完成对事件监听的操作。CallCardUi也是定义在该类中的:

public interface CallCardUi extends Ui {
void setVisible(boolean on);
void setCallCardVisible(boolean visible);
void setPrimary(String number, String name, boolean nameIsNumber, String label,
Drawable photo, boolean isSipCall);
void setSecondary(boolean show, String name, boolean nameIsNumber, String label,
String providerLabel, boolean isConference, boolean isVideoCall);
...
}


Ui接口中定义了Presenter中需要调用的操作。再来看CallCardFragment

@Override
public CallCardPresenter createPresenter() {
return new CallCardPresenter();
}
BaseFragment创建的实际上是CallCardPresenter,

@Override
public CallCardPresenter.CallCardUi getUi() {
return this;
}
接口Ui实际上是this,即CallCardFragment实现了CallCardUi接口。
这个是MVP模式的标准实现,实现了View(UI,对callcard来说是CallCardFragment)和Module(可以看成是InCallPresenter,UI更新和显示的数据和逻辑都来源于此)的分离,所有的逻辑流程都在Presenter中(对callcard来说是CallCardPresenter)。拿联系人名字name设置举例,CallCardFragment只负责怎么显示name(横着还是竖着,多大字号,什么字体,控件是TextView还是拿画板直接画);InCallPresenter负责通知Presenter
name有变,该更新UI了(例如有新的来电了,该显示来电姓名);CallCardPresenter负责具体的功能实现和Ui状态维护(例如获取Moudle层的通知,怎么从Moudle层查询到name,当前是单路通话还是多路通话,该调用哪个Ui接口做更新操作),Presenter隔离开View和Module。

InCallUI中的其他Fragment全是这个模式,这里只是拿CallCard举例。

CallCard

CallCardFragment

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Trace.beginSection(TAG + " onCreate");
mTranslationOffset =
getResources().getDimensionPixelSize(R.dimen.call_card_anim_translate_y_offset);
final View view = inflater.inflate(R.layout.call_card_fragment, container, false);
Trace.endSection();
return view;
}
布局文件是call_card_fragment.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:background="@color/callcard_fragment_background_color">

<!-- The main content of the CallCard is either one or two "call info"
blocks, depending on whether one or two lines are in use.

The call_info blocks are stacked vertically inside a CallCard (LinearLayout),
each with layout_weight="1".  If only one line is in use (i.e. the
common case) then the 2nd call info will be GONE and thus the 1st one
will expand to fill the full height of the CallCard. -->

<!-- Primary "call card" block, for the foreground call. -->
<LinearLayout      <!-- 最常用的布局,就是前台通话的通话布局 -->
android:id="@+id/primary_call_info_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:elevation="@dimen/primary_call_elevation"
android:layout_centerHorizontal="true"
android:background="@color/incall_call_banner_background_color"
android:paddingTop="@dimen/call_banner_primary_call_container_top_padding"
android:clipChildren="false"
android:clipToPadding="false">

<include layout="@layout/primary_call_info" />  <!-- 前台通话信息,包括名字、号码等等 -->

<fragment android:name="com.android.incallui.CallButtonFragment" <!-- 通话按键fragment -->
android:id="@+id/callButtonFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<TextView android:id="@+id/connectionServiceMessage" <!-- 紧急拨号相关,不过看代码中这个并没有使用 -->
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:visibility="gone"
android:padding="6dp"
android:background="@android:color/white" />

</LinearLayout>

<!-- M: ALPS01844813 primaryCallPhotoOrVideo id is used for RCSe plugin -->
<FrameLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_below="@id/primary_call_info_container"
android:id="@+id/primaryCallPhotoOrVideo"
>

<!-- Contact photo for primary call info -->
<ImageView android:id="@+id/photo" <!-- 头像 -->
android:layout_below="@id/primary_call_info_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:gravity="top|center_horizontal"
android:scaleType="centerCrop"
android:importantForAccessibility="no"
android:background="@android:color/white"
android:src="@drawable/img_no_image_automirrored" />

<!-- manage conference call button -->
<include layout="@layout/manage_conference_call_button" <!-- 会议管理 -->
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/primary_call_info_container" />

<!-- M:[Voice Record]record icon -->
<!-- M:fix ALPS02297097,recording icon still at the right
side as Arabic,correct layout_gravity from right to end -->
<ImageView android:id="@+id/voiceRecorderIcon"  <!-- 通话录音标记 -->
android:layout_width="@dimen/incall_record_icon_size"
android:layout_height="@dimen/incall_record_icon_size"
android:layout_gravity="end"
android:layout_marginEnd="10dip"
android:layout_marginTop="10dip"
android:visibility="gone" />

</FrameLayout>

<fragment android:name="com.android.incallui.VideoCallFragment" <!-- 视频通话布局 -->
android:id="@+id/videoCallFragment"
android:layout_alignParentTop="true"
android:layout_gravity="top|center_horizontal"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:gravity="top|center_horizontal"
android:scaleType="centerCrop"
android:contentDescription="@string/contactPhoto"
android:background="@android:color/white"
android:src="@drawable/img_no_image_automirrored" />

<!-- Progress spinner, useful for indicating pending operations such as upgrade to video. -->
<FrameLayout     <!-- 进度框,耗时操作时显示 -->
android:id="@+id/progressSpinner"
android:layout_below="@id/primary_call_info_container"
android:background="#63000000"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:visibility="gone">

<ProgressBar
android:id="@+id/progress_bar"
style="@android:style/Widget.Material.ProgressBar"
android:layout_gravity="center"
android:layout_width="48dp"
android:layout_height="48dp"
android:indeterminate="true" />

</FrameLayout>

<!-- Secondary "Call info" block, for the background ("on hold") call. -->
<include layout="@layout/secondary_call_info" /> <!-- 后台通话信息,区域就没有第一路那么大了 -->

<!-- Placeholder for various fragments that are added dynamically underneath the caller info. -->
<FrameLayout  <!-- 加载来电和拨号盘fragment的地方 -->
android:id="@+id/answer_and_dialpad_container"
android:layout_below="@id/primary_call_info_container"
android:layout_gravity="bottom|center_horizontal"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="@dimen/dialpad_elevation" />

<FrameLayout <!-- 底部挂断通话按键 -->
android:id="@+id/floating_end_call_action_button_container"
android:layout_width="@dimen/end_call_floating_action_button_diameter"
android:layout_height="@dimen/end_call_floating_action_button_diameter"
android:background="@drawable/fab_red"
android:layout_centerHorizontal="true"
android:layout_marginBottom="@dimen/end_call_button_margin_bottom"
android:layout_alignParentBottom="true" >

<ImageButton android:id="@+id/floating_end_call_action_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/end_call_background"
android:src="@drawable/fab_ic_end_call"
android:scaleType="center"
android:contentDescription="@string/onscreenEndCallText" />

</FrameLayout>

</RelativeLayout>


从xml看,这个的确是主界面,因为来电、拨号盘和按键三个fragment都是在这个布局中定义了加载的地方(其中按键fragment直接加载了,而来电和拨号盘还没有加载,只是定义了加载容器,且这两个fragment共用一个容器),这也解释了InCallActivity中的getFragmentManagerForTag为什么有两种不同的返回结果。
private FragmentManager getFragmentManagerForTag(String tag) {
if (TAG_DIALPAD_FRAGMENT.equals(tag)) {
return mChildFragmentManager;
} else if (TAG_ANSWER_FRAGMENT.equals(tag)) {
return mChildFragmentManager;
} else if (TAG_CONFERENCE_FRAGMENT.equals(tag)) {
return getFragmentManager();
} else if (TAG_CALLCARD_FRAGMENT.equals(tag)) {
return getFragmentManager();
}
throw new IllegalStateException("Unexpected fragment: " + tag);
}
注意拨号盘和来电fragment返回的是mChildFragmentManager。其余最重要的就是primary_call_info和secondary_call_info,分别代表了前台和后台通话信息。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/primary_call_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:minHeight="@dimen/call_banner_height"
android:paddingStart="@dimen/call_banner_side_padding"
android:paddingEnd="@dimen/call_banner_side_padding"
android:clipChildren="false"
android:clipToPadding="false"
android:animateLayoutChanges="true"
android:gravity="center">

<LinearLayout android:id="@+id/callStateButton"      <!-- 通话状态信息 -->
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:clipChildren="false"
android:clipToPadding="false">

<!-- Subscription provider or WiFi calling icon displayed to the left of the label -->
<ImageView android:id="@+id/callStateIcon"  <!-- 通话状态图标 -->
android:layout_width="24dp"
android:layout_height="match_parent"
android:layout_marginEnd="10dp"
android:tint="@color/incall_accent_color"
android:alpha="0.0"
android:scaleType="fitCenter"
android:visibility="gone" />

<ImageView android:id="@+id/videoCallIcon"   <!-- 视频通话图标 -->
android:src="@drawable/ic_toolbar_video"
android:layout_width="16dp"
android:layout_height="match_parent"
android:layout_marginEnd="16dp"
android:baselineAlignBottom="true"
android:tint="@color/incall_accent_color"
android:scaleType="center"
android:visibility="gone" />

<com.android.phone.common.widget.ResizingTextTextView  <!-- 通话状态,例如通话中、来电等等 -->
xmlns:ex="http://schemas.android.com/apk/res-auto"
android:id="@+id/callStateLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/incall_accent_color"
android:textSize="@dimen/call_status_text_size"
android:alpha="0.7"
android:singleLine="true"
android:gravity="start"
android:ellipsize="end"
ex:resizing_text_min_size="@dimen/call_status_text_min_size" />

</LinearLayout>

<!-- M: Wrap Google default layout in a RelativeLayout
for OP01 customization -->
<RelativeLayout android:id="@+id/nameAndPhoto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- Name (or the phone number, if we don't have a name to display). -->
<com.android.phone.common.widget.ResizingTextTextView <!-- 名字 -->
xmlns:ex="http://schemas.android.com/apk/res-auto"
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-5dp"
android:fontFamily="sans-serif-light"
android:textAlignment="viewStart"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/call_name_text_size"
android:singleLine="true"
ex:resizing_text_min_size="@dimen/call_name_text_min_size" />
</RelativeLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:clipChildren="false"
android:clipToPadding="false">

<ImageView android:id="@+id/hdAudioIcon" <!-- hd音频图标 -->
android:src="@drawable/ic_hd_24dp"
android:layout_width="24dp"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:tint="@color/incall_call_banner_subtext_color"
android:scaleType="fitCenter"
android:visibility="gone" />

<!-- Label (like "Mobile" or "Work", if present) and phone number, side by side -->
<LinearLayout android:id="@+id/labelAndNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal">

<!--M: fix ALPS02341761, update text view content layout:
add properties: android:ellipsize="middle" and android:maxWidth="70dp"-->
<TextView android:id="@+id/label" <!-- 号码标记,如座机、手机等 -->
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/incall_call_banner_subtext_color"
android:textSize="@dimen/call_label_text_size"
android:singleLine="true"
android:ellipsize="middle"
android:maxWidth="70dp"
android:textDirection="ltr"
android:visibility="gone" />

<!--M: fix ALPS02341761, update text view content layout:
add properties: android:ellipsize="start"; update: android:singleLine="false" to be "true"-->
<TextView android:id="@+id/phoneNumber" <!-- 号码 -->
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="6dp"
android:textAlignment="viewStart"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/incall_call_banner_subtext_color"
android:textSize="@dimen/call_label_text_size"
android:singleLine="true"
android:ellipsize="start"
android:visibility="gone" />

</LinearLayout>

<!-- Elapsed time indication for a call in progress. -->
<TextView android:id="@+id/elapsedTime" <!-- 通话时间 -->
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:textAlignment="viewEnd"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/incall_call_banner_subtext_color"
android:textSize="@dimen/call_label_text_size"
android:singleLine="true"
android:visibility="gone" />

</LinearLayout>

<!-- Call type indication: a special label and/or branding
for certain kinds of calls (like "SIP call" for a SIP call.) -->
<TextView android:id="@+id/callTypeLabel"  <!-- 这个没使用过,国内无SIP通话呀 -->
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/incall_call_banner_text_color"
android:maxLines="1"
android:ellipsize="end"
android:visibility="gone" />

</LinearLayout>
primary_call_info.xml可见是详细的通话信息展示,secondary_call_info类似,不过区域就没有这么大了,是个横着的长条。

// Primary caller info
private TextView mPhoneNumber;
private TextView mNumberLabel;
private TextView mPrimaryName;
private View mCallStateButton;
private ImageView mCallStateIcon;
private ImageView mCallStateVideoCallIcon;
private TextView mCallStateLabel;
private TextView mCallTypeLabel;
private ImageView mHdAudioIcon;
private View mCallNumberAndLabel;
private ImageView mPhoto;
private TextView mElapsedTime;
private Drawable mPrimaryPhotoDrawable;
上面是类中的部分成员,分析过xml后一看就知道这些是对应于xml布局中的控件,类的功能就是设置这些控件的显示内容、可见性及动画,没什么难点,所以代码不再做分析。

CallCardPresenter

主要的成员有:
private Call mPrimary;  //前台通话
private Call mSecondary; //后台通话
private ContactCacheEntry mPrimaryContactInfo; //前台通话信息
private ContactCacheEntry mSecondaryContactInfo; //后天通话信息
private CallTimer mCallTimer; //通话时长
查询联系人信息的方法是:
private void startContactInfoSearch(final Call call, final boolean isPrimary,
boolean isIncoming) {
final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);

cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary));
}
isPrimary区分前台还是后台通话,ContactLookupCallback实现了回调(查询完毕后会在回调中赋值给mPrimaryContactInfo或者mSecondaryContactInfo)。ContactInfoCache的查询具体流程见点击打开链接
@Override
public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
...
Call primary = null;
Call secondary = null;

if (newState == InCallState.INCOMING) { //依据不同状态获取前台通话和后台通话
primary = callList.getIncomingCall();
} else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) {
primary = callList.getOutgoingCall();
if (primary == null) {
primary = callList.getPendingOutgoingCall();
}

// getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
// highest priority call to display as the secondary call.
secondary = getCallToDisplay(callList, null, true);
} else if (newState == InCallState.INCALL) {
primary = getCallToDisplay(callList, null, false);
secondary = getCallToDisplay(callList, primary, true);
}
...
mSecondary = secondary;
Call previousPrimary = mPrimary;
mPrimary = primary;
...
...
mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mPrimary,
mPrimary.getState() == Call.State.INCOMING);  //注意这个方法很坑,这个其实并没有做数据库查询,只是返回个暂时能用的信息
updatePrimaryDisplayInfo(); //更新前台通话
maybeStartSearch(mPrimary, true); //这个才是真正的查询,在查询完毕后会真正的更新联系人信息
...
if (mSecondary == null) { //更新后台通话
// Secondary call may have ended.  Update the ui.
mSecondaryContactInfo = null; //信息为null,则不显示后台通话
updateSecondaryDisplayInfo();
} else if (secondaryChanged) {  //与前台类似
// secondary call has changed
mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mSecondary,
mSecondary.getState() == Call.State.INCOMING);
Call.State.isIncoming(mSecondary.getState()));
updateSecondaryDisplayInfo();
maybeStartSearch(mSecondary, false);
mSecondary.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
}

// Start/stop timers.
if (isPrimaryCallActive()) { //启动或者停止通话时长显示
Log.d(this, "Starting the calltime timer");
mCallTimer.start(CALL_TIME_UPDATE_INTERVAL_MS);
} else {
Log.d(this, "Canceling the calltime timer");
mCallTimer.cancel();
ui.setPrimaryCallElapsedTime(false, 0);
}
...
}
onStateChange是最常用的更新UI的入口方法,之前相关的几个成员都在这个方法中出现了,具体见注释。
CallTime继承自Handler,每秒post一个Runnable维持时间的变化。
public CallCardPresenter() {
// create the call timer
mCallTimer = new CallTimer(new Runnable() {
@Override
public void run() {
updateCallTime();
}
});
}
这个Runnable在构造方法中传递进去的,就是用updateCallTime设置时间。

CallButton

CallButtonFragment

packages/apps/InCallUI/src/com/android/incallui/CallButtonFragment.java

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View parent = inflater.inflate(R.layout.call_button_fragment, container, false);
...
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/bottomButtons"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_margin="0dp"
android:padding="0dp"
android:background="@color/button_background_color"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:animateLayoutChanges="true" >

<!-- M: ALPS01844813 callButtonContainer id is used for RCSe plugin -->
<LinearLayout
android:id="@+id/callButtonContainer"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom|center_horizontal"
android:baselineAligned="false">

<!-- FAR LEFT SLOT ===================================================================== -->

<!-- "Audio mode". this is a multi-mode button that can behave either like a simple
"compound button" with two states *or* like an action button that brings up a popup
menu; see btn_compound_audio.xml and CallButtonFragment.updateAudioButtons(). -->
<ToggleButton android:id="@+id/audioButton"  <!-- 音频按键,一般情况下就是免提按键,插耳机的情况下会弹出选择菜单 -->
style="@style/InCallCompoundButton"
android:background="@drawable/btn_compound_audio"
android:contentDescription="@string/audio_mode_speaker" />

<!-- MIDDLE LEFT SLOT ================================================================== -->

<!-- "Mute" -->
<ToggleButton android:id="@+id/muteButton" <!-- 静音 -->
style="@style/InCallCompoundButton"
android:background="@drawable/btn_compound_mute"
android:contentDescription="@string/onscreenMuteText" />

<!-- CENTER SLOT ======================================================================= -->

<!-- "Dialpad" -->
<ToggleButton android:id="@+id/dialpadButton" <!-- dtmf拨号盘 -->
style="@style/InCallCompoundButton"
android:background="@drawable/btn_compound_dialpad"
android:contentDescription="@string/onscreenShowDialpadText" />

<!-- MIDDLE RIGHT SLOT ================================================================= -->

<!-- This slot is either "Hold" or "Swap", depending on the state of the call. One or the
other of these must always be set to GONE. -->

<!-- "Hold" -->
<ToggleButton android:id="@+id/holdButton" <!-- 保持通话 -->
style="@style/InCallCompoundButton"
android:background="@drawable/btn_compound_hold"
android:contentDescription="@string/onscreenHoldText_unselected" />

<!-- "Swap" (or "Manage calls" in some CDMA states) -->
<ImageButton android:id="@+id/swapButton" <!-- 交换通话 -->
style="@style/InCallButton"
android:background="@drawable/btn_swap"
android:contentDescription="@string/onscreenSwapCallsText"
android:visibility="gone" />

<!-- "Change to video call" -->
<ImageButton android:id="@+id/changeToVideoButton" <!-- 切换到视频通话 -->
style="@style/InCallButton"
android:background="@drawable/btn_change_to_video"
android:contentDescription="@string/onscreenVideoCallText"
android:visibility="gone" />

<!-- "Switch camera" for video calls. -->
<ToggleButton android:id="@+id/switchCameraButton" <!-- 切换摄像头 -->
style="@style/InCallCompoundButton"
android:background="@drawable/btn_compound_video_switch"
android:contentDescription="@string/onscreenSwitchCameraText"
android:visibility="gone" />

<!-- FAR RIGHT SLOT ==================================================================== -->

<!-- This slot is either "Add" or "Merge", depending on the state of the call.  One or the
other of these must always be set to GONE. -->

<!-- "Turn off camera" for video calls. -->
<ToggleButton android:id="@+id/pauseVideoButton" <!-- 关闭视频 -->
style="@style/InCallCompoundButton"
android:background="@drawable/btn_compound_video_off"
android:contentDescription="@string/onscreenPauseVideoText"
android:visibility="gone" />

<!-- "Add Call" -->
<ImageButton android:id="@+id/addButton"  <!-- 添加通话 -->
style="@style/InCallButton"
android:background="@drawable/btn_add"
android:contentDescription="@string/onscreenAddCallText"
android:visibility="gone" />

<!-- "Merge calls". This button is used only on GSM devices, where we know that "Add" and
"Merge" are never available at the same time. The "Merge" button for CDMA devices is
"cdmaMergeButton" above. -->
<ImageButton android:id="@+id/mergeButton"  <!-- 合并通话 -->
style="@style/InCallButton"
android:background="@drawable/btn_merge"
android:contentDescription="@string/onscreenMergeCallsText"
android:visibility="gone" />

<!-- "Overflow" -->
<ImageButton android:id="@+id/overflowButton" <!-- 三个点的菜单按键,当按键过多时显示 -->
style="@style/InCallButton"
android:background="@drawable/btn_overflow"
android:contentDescription="@string/onscreenOverflowText"
android:visibility="gone" />

<!-- M: "Change to voice call" -->
<ImageButton android:id="@+id/changeToVoiceButton" <!-- 切换到音频通话 -->
style="@style/InCallButton"
android:background="@drawable/btn_change_to_video"
android:contentDescription="@string/onscreenChangeToVoiceText"
android:visibility="gone" />

<!-- M :"Hide Local preview" -->
<ToggleButton android:id="@+id/hideOrShowLocalVideo" <!-- 隐藏本地视频显示 -->
style="@style/InCallCompoundButton"
android:contentDescription="@string/hideVideoPreview"
android:visibility="gone" />

<!-- "Manage conference button (Video Call) " -->
<ImageButton android:id="@+id/manageVideoCallConferenceButton" <!-- 管理视频会议通话 -->
style="@style/InCallButton"
android:background="@drawable/ic_group_white_24dp"
android:contentDescription="@string/onscreenManageConferenceText"
android:visibility="gone" />

<!-- Mediatek add start-->
<!-- "Set ect button " -->
<ImageButton android:id="@+id/setEctButton" <!-- 转接 -->
style="@style/InCallButton"
android:background="@drawable/mtk_btn_transfer"
android:contentDescription="@string/menu_ect"
android:visibility="gone" />

<!-- "Hangup all calls button " -->
<ImageButton android:id="@+id/hangupAllCallsButton" <!-- 挂断所有通话 -->
style="@style/InCallButton"
android:background="@drawable/mtk_btn_hangup_all"
android:contentDescription="@string/onscreenHangupAll"
android:visibility="gone" />

<!-- Hangup all hold calls button " -->
<ImageButton android:id="@+id/hangupAllHoldCallsButton" <!-- 挂断所有后台通话 -->
style="@style/InCallButton"
android:background="@drawable/mtk_btn_hangup_all_holding"
android:contentDescription="@string/onscreenHangupHolding"
android:visibility="gone" />

<!-- "Hangup active and answer waiting button " -->
<ImageButton android:id="@+id/hangupActiveAndAnswerWaitingButton" <!-- 挂断当前通话并接听来电 -->
style="@style/InCallButton"
android:background="@drawable/mtk_btn_hangup_active_answer_waiting"
android:contentDescription="@string/onscreenHangupActiveAndAnswerWaiting"
android:visibility="gone" />

<!-- [Voice Record] start/stop voice record button --> <!-- 通话录音 -->
<ToggleButton android:id="@+id/switch_voice_record"
style="@style/InCallButton"
android:background="@drawable/mtk_btn_compound_voice_record"
android:gravity="center"
android:contentDescription="@string/start_record"
android:visibility="gone" />
<!-- Mediatek add end-->

</LinearLayout>

</LinearLayout>


CallButtonPresenter

packages/apps/InCallUI/src/com/android/incallui/CallButtonPresenter.java
@Override
public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
...
updateUi(newState, mCall);
...
}
onStateChange依然是更新UI的入口。
private void updateUi(InCallState state, Call call) {
...
updateButtonsState(call);
...
}
private void updateButtonsState(Call call) {
...
final boolean showSwap = call.can(
android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE);
...
ui.showButton(BUTTON_SWAP, showSwap);
...
}
这里只以交换按键展示了更新UI的流程。

answer

AnswerFragment

packages/apps/InCallUI/src/com/android/incallui/AnswerFragment.java

布局是answer_fragment,

<com.android.incallui.GlowPadWrapper
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dc="http://schemas.android.com/apk/res-auto"
android:id="@+id/glow_pad_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:layout_centerHorizontal="true"
android:gravity="center"
android:background="@color/glowpad_background_color"
android:layout_marginBottom="@dimen/glowpadview_margin_bottom"

dc:targetDrawables="@array/incoming_call_widget_audio_with_sms_targets"
dc:targetDescriptions="@array/incoming_call_widget_audio_with_sms_target_descriptions"
dc:directionDescriptions="@array/incoming_call_widget_audio_with_sms_direction_descriptions"
dc:handleDrawable="@drawable/ic_incall_audio_handle"
dc:outerRingDrawable="@drawable/ic_lockscreen_outerring"
dc:outerRadius="@dimen/glowpadview_target_placement_radius"
dc:innerRadius="@dimen/glowpadview_inner_radius"
dc:snapMargin="@dimen/glowpadview_snap_margin"
dc:feedbackCount="1"
dc:vibrationDuration="20"
dc:glowRadius="@dimen/glowpadview_glow_radius"
dc:pointDrawable="@drawable/ic_lockscreen_glowdot"
dc:allowScaling="true" />
这个xml就比上面的简单多了,就一个控件packages/apps/InCallUI/src/com/android/incallui/GlowPadWrapper.java,该类继承packages/apps/InCallUI/src/com/android/incallui/widget/multiwaveview/GlowPadView.java,主要是处理ping事件和实现了onTrigger,绝大部分工作都在基类中完成。GlowPadView比较复杂,会单开一篇文章介绍。通过这个控件可以用拖动的放式实现接听来电、挂断来电和短信拒绝。

AnswerPresenter

packages/apps/InCallUI/src/com/android/incallui/AnswerPresenter.java

该类总共也就421行代码,最主要的是

@Override
public void onIncomingCall(InCallState oldState, InCallState newState, Call call) {
...
processIncomingCall(call);
...
}
private void processIncomingCall(Call call) {
...
if (showAnswerUi(true)) {   //显示AnswerFragment,实际就是调用InCallActivity的showAnswerFragment方法
final List<String> textMsgs = CallList.getInstance().getTextResponses(call.getId());
configureAnswerTargetsForSms(call, textMsgs); //设置GlowPadWrapper按下后出现的选项
}
...
}


private void configureAnswerTargetsForSms(Call call, List<String> textMsgs) {
...
mHasTextMessages = textMsgs != null;
boolean withSms =
call.can(android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT)
&& mHasTextMessages;
...
if (withSms) {
getUi().showTargets(AnswerFragment.TARGET_SET_FOR_AUDIO_WITH_SMS);
getUi().configureMessageDialog(textMsgs);
} else {
getUi().showTargets(AnswerFragment.TARGET_SET_FOR_AUDIO_WITHOUT_SMS);
}
...
}
依据textMsgs是否为null来设置GlowPadWrapper按下后是否显示短信拒接。

dialpad

DialpadFragment

packages/apps/InCallUI/src/com/android/incallui/DialpadFragment.java

布局文件incall_dialpad_fragment.xml

<view class="com.android.incallui.DialpadFragment$DialpadSlidingLinearLayout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dtmf_twelve_key_dialer_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/dialpad_view"/>
</view>
其中DialpadSlidingLinearLayout是为了实现拨号盘的的动画效果

public static class DialpadSlidingLinearLayout extends LinearLayout {

public DialpadSlidingLinearLayout(Context context) {
super(context);
}

public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}

public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}

public float getYFraction() {
final int height = getHeight();
if (height == 0) return 0;
return getTranslationY() / height;
}

public void setYFraction(float yFraction) {
setTranslationY(yFraction * getHeight());
}
}
可以通过控制YFraction属性实现某种动画,但是这个DialpadFragment同名类在Dialer目录下有用到这个属性,InCallUI目录下并没有使用这个属性,所以InCallUI中可以把DialpadSlidingLinearLayout看成一个普通的LinearLayout就对了。从这个copy代码的尿性看应该是mtk干的。

/home/lgy/code/mtk6797/packages/apps/PhoneCommon/res/layout/dialpad_view.xml

<merge xmlns:android="http://schemas.android.com/apk/res/android">
<include
layout="@layout/dialpad_view_unthemed"
android:theme="@style/Dialpad_Light" />
</merge>
<view class="com.android.phone.common.dialpad.DialpadView"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dialpad_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_gravity="bottom"
android:orientation="vertical"
android:layoutDirection="ltr"
android:background="?attr/dialpad_background"
android:clickable="true">

<!-- Text field where call rate is displayed for ILD calls. -->
<LinearLayout
android:id="@+id/rate_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">

<LinearLayout
android:id="@+id/ild_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/ild_margin_height"
android:layout_marginBottom="@dimen/ild_margin_height">

<TextView android:id="@+id/ild_country"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

<TextView android:id="@+id/ild_rate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:textStyle="bold" />

</LinearLayout>

<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#e3e3e3" />

</LinearLayout>

<!-- Text field and possibly soft menu button above the keypad where
the digits are displayed. -->
<LinearLayout
android:id="@+id/digits_container"
android:layout_width="match_parent"
android:layout_height="@dimen/dialpad_digits_adjustable_height"
android:orientation="horizontal">

<ImageButton android:id="@+id/dialpad_overflow"   <!-- 菜单键,dtmf拨号盘中用不到 -->
android:background="@drawable/btn_dialpad_key"
android:src="@drawable/ic_overflow_menu"
android:tint="?attr/dialpad_icon_tint"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_margin="@dimen/dialpad_overflow_margin"
android:paddingLeft="@dimen/dialpad_digits_menu_left_padding"
android:paddingRight="@dimen/dialpad_digits_menu_right_padding"
android:contentDescription="@string/description_dialpad_overflow"
android:gravity="center"
android:visibility="invisible" />

<view class="com.android.phone.common.dialpad.DigitsEditText"   <!-- 上方显示的数字EditText -->
xmlns:ex="http://schemas.android.com/apk/res-auto"
android:id="@+id/digits"
android:layout_width="0dp"
android:layout_height="match_parent"
android:scrollHorizontally="true"
android:singleLine="true"
android:layout_weight="1"
android:gravity="center"
android:background="@android:color/transparent"
android:maxLines="1"
android:textSize="@dimen/dialpad_digits_adjustable_text_size"
android:freezesText="true"
android:focusableInTouchMode="true"
android:cursorVisible="false"
android:textColor="?attr/dialpad_text_color"
android:textCursorDrawable="@null"
android:fontFamily="sans-serif"
android:textStyle="normal"
ex:resizing_text_min_size="@dimen/dialpad_digits_text_min_size" />

<ImageButton
android:id="@+id/deleteButton"   <!-- 删除输入按键 -->
android:background="@drawable/btn_dialpad_key"
android:tint="?attr/dialpad_icon_tint"
android:paddingLeft="@dimen/dialpad_digits_padding"
android:paddingRight="@dimen/dialpad_digits_padding"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:state_enabled="false"
android:contentDescription="@string/description_delete_button"
android:src="@drawable/ic_dialpad_delete" />
</LinearLayout>

<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#e3e3e3" />

<Space
android:layout_width="match_parent"
android:layout_height="@dimen/dialpad_space_above_keys" />

<include layout="@layout/dialpad" />  <!-- 12键布局 -->

<Space
android:layout_width="match_parent"
android:layout_height="8dp" />

</view>
这个布局是和Dialer的主界面公用的,所以有些东西是dtmf拨号盘用不到的。
packages/apps/PhoneCommon/res/layout/dialpad.xml,12键布局就是4*3的布局,简单不再阐述。

DialpadPresenter

/home/lgy/code/mtk6797/packages/apps/InCallUI/src/com/android/incallui/DialpadPresenter.java

103行的文件,最重要的就是按键处理:

public final void processDtmf(char c) {
Log.d(this, "Processing dtmf key " + c);
// if it is a valid key, then update the display and send the dtmf tone.
if (PhoneNumberUtils.is12Key(c) && mCall != null) {
Log.d(this, "updating display and sending dtmf tone for '" + c + "'");

// Append this key to the "digits" widget.
getUi().appendDigitsToField(c);
// Plays the tone through Telecomm.
TelecomAdapter.getInstance().playDtmfTone(mCall.getId(), c);
} else {
Log.d(this, "ignoring dtmf request for '" + c + "'");
}
}
基本所有的对下的操作都是通过packages/apps/InCallUI/src/com/android/incallui/TelecomAdapter.java来处理的,它直接调用到了telecom framework中,然后调用到Telecom.apk代码中,最后再到TeleService.apk中,跨越了3个进程。

其它

还有视频通话VideoCallFragment、会议管理ConferenceManagerFragment和PT拨号盘PostCharDialogFragment三个fragment没有介绍,这三个一般用户使用几率非常低,可以说是不用,不再详细分析了。还有些UI元素例如没有设置默认拨号卡后会弹出的选卡对话框SelectAccountDialogFragment(位于ContactsCommon包),控件GlowPadView等等以后有兴趣会分析。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Android incallui