likes
comments
collection
share

Android Widget(小部件)添加源码分析三--创建视图

作者站长头像
站长
· 阅读数 14

本文是 Android Widget(小部件) 系列的第七篇,主要从源码角度是对 Android widget 添加过程进行分析 。

本系列的目的是通过对Android 小部件的梳理,了解小部件刷新流程、添加、删除、恢复流程、以及系统发生变化时,小部件是如何适配的,解决在开发小部件过程中遇到的问题。系列文章大部份来自源码的解读,内容非常多,也非常容易遗忘,因此记录分享。

系列文章

一、描述

添加小部件通常是 host 的逻辑,但了解该过程能够帮助我们熟悉小部件体系,分析小部件问题。由于每个 host 对小部件的逻辑处理都不一样,因此本文不梳理 host 中添加逻辑,仅从 ### AppWidgetHost .allocateAppWidgetId 开始。添加 widget 分三步、本篇文章讲解第三步创建视图

1.  申请widgetId
2.  绑定widgetId
3.  创建WidgetHostView

//1、申请
int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
//2、绑定
bound = mAppWidgetManager.bindAppWidgetIdIfAllowed(
appWidgetId, providerInfo.getProfile(), providerInfo.provider,bundle)
//3、创建视图
WidgetHostView hostView = (WidgetHostView) mAppWidgetHost.createView(mContext, appWidgetId ,
appWidgetProviderInfo);

二、创建视图流程

Android Widget(小部件)添加源码分析三--创建视图

1、Host 进程通过 AIDL 回调到 system_server 中的AppWidgetServiceImpl 
2、进行安全校验和小部件加载确定
3、找到对应的 widget ,返回对应的 RemoteView
4、将RemoteView 中的 action 转换成对应的视图 

三、详细流程

1、AppWidgetServiceImpl.getAppWidgetViews()

描述:通过AIDL 访问系统服务获取 RemoteView 详细代码:

class AppWidgetServiceImpl{
@Override
public RemoteViews getAppWidgetViews(String callingPackage, int appWidgetId) {
    final int userId = UserHandle.getCallingUserId();

    if (DEBUG) {
        Slog.i(TAG, "getAppWidgetViews() " + userId);
    }

    // Make sure the package runs under the caller uid.
    // 确认安全性,前面已经分析
    mSecurityPolicy.enforceCallFromPackage(callingPackage);

    synchronized (mLock) {
        // 确定小部件已经加载,前面已经分析
        ensureGroupStateLoadedLocked(userId);

        // NOTE: The lookup is enforcing security across users by making
        // sure the caller can only access widgets it hosts or provides.
        // 找到对应的widget 前面已经分析
        Widget widget = lookupWidgetLocked(appWidgetId,
                Binder.getCallingUid(), callingPackage);
       // 找到对应的widget,clone RemoteView 进行返回
        if (widget != null) {
            return cloneIfLocalBinder(widget.getEffectiveViewsLocked());
        }

        return null;
    }
}

2、AppWidgetHostView. updateAppWidget()

描述:根据RemoteView 设置视图 详细代码

class AppWidgetHostView{
    public void updateAppWidget(RemoteViews remoteViews) {
        mLastInflatedRemoteViews = remoteViews;
        applyRemoteViews(remoteViews, true);
    }
    
    
    protected void applyRemoteViews(@Nullable RemoteViews remoteViews, boolean useAsyncIfPossible) {
    boolean recycled = false;
    View content = null;
    Exception exception = null;

    // Block state restore until the end of the apply.
    mLastInflatedRemoteViewsId = -1;

    if (mLastExecutionSignal != null) {
        mLastExecutionSignal.cancel();
        mLastExecutionSignal = null;
    }
   
    if (remoteViews == null) {
     // remoteView 为null 加载默认视图apk 升级、组件状态改变,触发
        if (mViewMode == VIEW_MODE_DEFAULT) {
            // We've already done this -- nothing to do.
            return;
        }
        content = getDefaultView();
        mLayoutId = -1;
        mViewMode = VIEW_MODE_DEFAULT;
    } else {
        // Select the remote view we are actually going to apply.
        //找到合适的RemoteView ,方法内部会屏幕方向,以及尺寸去找合适的RemoteView
        RemoteViews rvToApply = remoteViews.getRemoteViewsToApply(mContext, mCurrentSize);
        if (mOnLightBackground) {
            rvToApply = rvToApply.getDarkTextViews();
        }
        // 异步加载,如果没有设置异步线程池的话,不会走该逻辑
        if (mAsyncExecutor != null && useAsyncIfPossible) {
            inflateAsync(rvToApply);
            return;
        }
        int layoutId = rvToApply.getLayoutId();
        // 判断是否可以重新使用用之前的view,这里是比较布局id 及viewID与之前的是否相同。widget 是可以添加一个RemoteView
        // 的,因此这里需要比较viewID。
        if (rvToApply.canRecycleView(mView)) {
            try {
                // 将Remote action 设置在 HostView 上。
                rvToApply.reapply(mContext, mView, mInteractionHandler, mCurrentSize,
                        mColorResources);
                content = mView;
                mLastInflatedRemoteViewsId = rvToApply.computeUniqueId(remoteViews);
                recycled = true;
                if (LOGD) Log.d(TAG, "was able to recycle existing layout");
            } catch (RuntimeException e) {
                exception = e;
            }
        }

        // Try normal RemoteView inflation
        if (content == null) {
            try {
                // 该方法会inflate view ,然后将RemoteView 设置到view上
                content = rvToApply.apply(mContext, this, mInteractionHandler,
                        mCurrentSize, mColorResources);
                mLastInflatedRemoteViewsId = rvToApply.computeUniqueId(remoteViews);
                if (LOGD) Log.d(TAG, "had to inflate new layout");
            } catch (RuntimeException e) {
                exception = e;
            }
        }

        mLayoutId = layoutId;
        mViewMode = VIEW_MODE_CONTENT;
    }

    applyContent(content, recycled, exception);
  }
}
3、AppWidgetHostView. getDefaultView()

描述:获取配置文件中的 initialLayout 视图,如果没有设置,将返回错误视图 详细代码

class AppWidgetHostView{
    protected View getDefaultView() {
        if (LOGD) {
            Log.d(TAG, "getDefaultView");
        }
        View defaultView = null;
        Exception exception = null;
    
        try {
            if (mInfo != null) {
                Context theirContext = getRemoteContext();
                mRemoteContext = theirContext;
                LayoutInflater inflater = (LayoutInflater)
                        theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                inflater = inflater.cloneInContext(theirContext);
                inflater.setFilter(INFLATER_FILTER);
                AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
                Bundle options = manager.getAppWidgetOptions(mAppWidgetId);
    
                int layoutId = mInfo.initialLayout;
                if (options.containsKey(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)) {
                    int category = options.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY);
                    if (category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) {
                        int kgLayoutId = mInfo.initialKeyguardLayout;
                        // If a default keyguard layout is not specified, use the standard
                        // default layout.
                        layoutId = kgLayoutId == 0 ? layoutId : kgLayoutId;
                    }
                }
                defaultView = inflater.inflate(layoutId, this, false);
                if (!(defaultView instanceof AdapterView)) {
                    // AdapterView does not support onClickListener
                    defaultView.setOnClickListener(this::onDefaultViewClicked);
                }
            } else {
                Log.w(TAG, "can't inflate defaultView because mInfo is missing");
            }
        } catch (RuntimeException e) {
            exception = e;
        }
    
        if (exception != null) {
            Log.w(TAG, "Error inflating AppWidget " + mInfo, exception);
        }
    
        if (defaultView == null) {
            if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error");
            defaultView = getErrorView();
        }
    
        return defaultView;
    }
}
4、RemoteViews. apply()
  • 描述:inflate view ,并且将 RemoteView 中的 action 转化成相应属性,设置到view 上。
  • 详细代码
class RemoteViews{
    public View apply(@NonNull Context context, @NonNull ViewGroup parent,
            @Nullable InteractionHandler handler, @Nullable SizeF size) {
        // 找到合适的RemoteView
        RemoteViews rvToApply = getRemoteViewsToApply(context, size);
        // 加载视图
        View result = inflateView(context, rvToApply, parent);
        rvToApply.performApply(result, parent, handler, null);
        return result;
    }
}
5、RemoteViews.getRemoteViewsToApply()
  • 描述:找到适合尺寸的widget,这里的 widgetSize 为null,只会根据横竖屏去选择对应的视图
  • 详细代码
class RemoteViews{
    public RemoteViews getRemoteViewsToApply(@NonNull Context context,
            @Nullable SizeF widgetSize) {
        if (!hasSizedRemoteViews() || widgetSize == null) {
            // If there isn't multiple remote views, fall back on the previous methods.
            return getRemoteViewsToApply(context);
        }
        return findBestFitLayout(widgetSize);
    }
    // 找到尺寸最合适的remoteview ,更近宽高的差距判断,寻找差距最小的
    private RemoteViews findBestFitLayout(@NonNull SizeF widgetSize) {
    // Find the better remote view
    RemoteViews bestFit = null;
    float bestSqDist = Float.MAX_VALUE;
    for (RemoteViews layout : mSizedRemoteViews) {
        SizeF layoutSize = layout.getIdealSize();
        if (layoutSize == null) {
            throw new IllegalStateException("Expected RemoteViews to have ideal size");
        }

        if (fitsIn(layoutSize, widgetSize)) {
            if (bestFit == null) {
                bestFit = layout;
                bestSqDist = squareDistance(layoutSize, widgetSize);
            } else {
                float newSqDist = squareDistance(layoutSize, widgetSize);
                if (newSqDist < bestSqDist) {
                    bestFit = layout;
                    bestSqDist = newSqDist;
                }
            }
        }
    }
    if (bestFit == null) {
        Log.w(LOG_TAG, "Could not find a RemoteViews fitting the current size: " + widgetSize);
        return findSmallestRemoteView();
    }
    return bestFit;
    }
}
6、RemoteViews. inflateView()
  • 描述:LayoutInflater加载视图。这里的context 通过 小部件宿主包名创建,因此可以获取对应资源文件。详细可看 application.getBaseContext().createPackageContextAsUser
  • 详细代码:
class RemoteViews {
    private Context getContextForResources(Context context) {
        if (mApplication != null) {
             // 如果是同一应用,就用该context
            if (context.getUserId() == UserHandle.getUserId(mApplication.uid)
                    && context.getPackageName().equals(mApplication.packageName)) {
                return context;
            }
            try {
                // remoteview 创建时,会根据packgeName 创建相应的appliction,后面有详细代码
                return context.createApplicationContext(mApplication,
                        Context.CONTEXT_RESTRICTED);
            } catch (NameNotFoundException e) {
                Log.e(LOG_TAG, "Package name " + mApplication.packageName + " not found");
            }
        }
    
        return context;
    }
   public RemoteViews(String packageName, int layoutId) {
    this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
   }
   
   private static ApplicationInfo getApplicationInfo(@Nullable String packageName, int userId) {
        if (packageName == null) {
            return null;
        }
    
        // Get the application for the passed in package and user.
        Application application = ActivityThread.currentApplication();
        if (application == null) {
            throw new IllegalStateException("Cannot create remote views out of an aplication.");
        }
    
        ApplicationInfo applicationInfo = application.getApplicationInfo();
        if (UserHandle.getUserId(applicationInfo.uid) != userId
                || !applicationInfo.packageName.equals(packageName)) {
            try {
                // 创建相应的context,用于访问相应的资源
                Context context = application.getBaseContext().createPackageContextAsUser(
                        packageName, 0, new UserHandle(userId));
                applicationInfo = context.getApplicationInfo();
            } catch (NameNotFoundException nnfe) {
                throw new IllegalArgumentException("No such package " + packageName);
            }
        }
    
        return applicationInfo;
    }
}
7、RemoteViews. performApply()
  • 描述:根据action 对view进行设置
  • 详细代码:
class RemoteViews{
   
    private void performApply(View v, ViewGroup parent, InteractionHandler handler,
            ColorResources colorResources) {
        if (mActions != null) {
            handler = handler == null ? DEFAULT_INTERACTION_HANDLER : handler;
            final int count = mActions.size();
            for (int i = 0; i < count; i++) {
                Action a = mActions.get(i);
                // 将action 设置在view 上,
                a.apply(v, parent, handler, colorResources);
            }
        }
    }
}
8、BaseReflectionAction.apply()
  • 描述:将action 进行设置,转成相应行为
  • 详细代码:
class BaseReflectionAction{

    @Override
    public final void apply(View root, ViewGroup rootParent, InteractionHandler handler,
            ColorResources colorResources) {
        final View view = root.findViewById(viewId);
        if (view == null) return;
    
        Class<?> param = getParameterType(this.type);
        if (param == null) {
            throw new ActionException("bad type: " + this.type);
        }
        Object value = getParameterValue(view);
        try {
             // getMethod 找到具体的方法,invoke 反射调用
            getMethod(view, this.methodName, param, false /* async */).invoke(view, value);
        } catch (Throwable ex) {
            throw new ActionException(ex);
        }
    }
}
9、AppWidgetHostView.applyContent()
  • 描述:将生成好的view添加到AppWidgetHostView中
  • 详细代码
class AppWidgetHostView {
    private void applyContent(View content, boolean recycled, Exception exception) {
        if (content == null) {
            if (mViewMode == VIEW_MODE_ERROR) {
                // We've already done this -- nothing to do.
                return ;
            }
            if (exception != null) {
                Log.w(TAG, "Error inflating RemoteViews", exception);
            }
            // 设置错误视图,之前已经分析过
            content = getErrorView();
            mViewMode = VIEW_MODE_ERROR;
        }
        // 没有复用的case
        if (!recycled) {
            prepareView(content);
            addView(content);
        }
    
        if (mView != content) {
            removeView(mView);
            mView = content;
        }
    }
}
10、AppWidgetHostView. prepareView()

描述:设置布局参数 详细代码:

 /** * Prepare the given view to be shown. This might include adjusting * {  @link  FrameLayout.LayoutParams} before inserting. */ protected void prepareView(View view) {
    // Take requested dimensions from child, but apply default gravity.
    FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams();
    if (requested == null) {
        requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
    }

    requested.gravity = Gravity.CENTER;
    view.setLayoutParams(requested);
}

到这里,添加 widget 创建视图流程就分析完了,也是添加 widget的最后一篇文章。添加 widget 可以简单的分为三步:申请widgetId -> 绑定widgetId-> 创建视图。总体流程比较长,拆解以后就会发现有很多校验、状态逻辑都是相同的,也就不那么难了。