当前位置: 首页 > news >正文

遵义酷虎网站开发电子商务网站建设精品课程

遵义酷虎网站开发,电子商务网站建设精品课程,一般建设网站大概需要多少钱,网站切片 做程序Android Settings 系列文章#xff1a; Android Settings解析SettingsIntelligenceSettingsProvider 首语 Android Settings中搜索功能帮助我们可以快速访问设置项#xff0c;进行自定义设置#xff0c;以得到更佳的使用体验。Android Settings搜索的实现实际不在Setting…Android Settings 系列文章 Android Settings解析SettingsIntelligenceSettingsProvider 首语 Android Settings中搜索功能帮助我们可以快速访问设置项进行自定义设置以得到更佳的使用体验。Android Settings搜索的实现实际不在Settings模块里而是存在一个单独的模块—SettingsIntelligence它里面实现了Settings的核心搜索功能因此学习SettingsIntelligence搜索实现可以让我们更多了解Settings模块。 搜索实现流程 本文以Android 13 SettingsIntelligence模块源码进行分析。 首先搜索栏的跳转实现在SearchFeatureProvider的initSearchToolbar中initSearchToolbar在Android Settings解析文章分析过在SettingsHomepageActivity的initSearchBarView方法中调用。最终跳转到包名为com.android.settings.intelligenceaction为android.settings.APP_SEARCH_SETTINGS的页面中。 public interface SearchFeatureProvider {default void initSearchToolbar(FragmentActivity activity, Toolbar toolbar, int pageId) {...final Intent intent buildSearchIntent(context, pageId).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);...toolbar.setOnClickListener(tb - {FeatureFactory.getFactory(context).getSlicesFeatureProvider().indexSliceDataAsync(context);FeatureFactory.getFactory(context).getMetricsFeatureProvider().logSettingsTileClick(KEY_HOMEPAGE_SEARCH_BAR, pageId);final Bundle bundle ActivityOptions.makeSceneTransitionAnimation(activity).toBundle();activity.startActivity(intent, bundle);});} }它对应的模块为SettingsIntelligence模块路径packages/apps/SettingsIntelligence。从AndroidManifest.xml可以看到Settings跳转搜索的页面为SearchActivitySearchActivity添加SearchFragment在SearchFragment中实现了搜索的核心逻辑。 查看onCreate方法进行了一些变量的初始化onCreateView方法中进行view初始化设置布局为search_panel我们只需要关注搜索框控件SearchView设置查询字符串为mQuery即输入搜索的内容。 设置查询监听重写onQueryTextSubmit和onQueryTextChange方法。当搜索框文本改变时通过restartLoaders方法调用LoadManager开启加载数据流程。当Loader创建成功时回调onCreateLoader方法调用getSearchResultLoader方法来SearchResultLoader实例。 public class SearchFragment extends Fragment implements SearchView.OnQueryTextListener,LoaderManager.LoaderCallbacksList? extends SearchResult, IndexingCallback {Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {... mSearchView toolbar.findViewById(R.id.search_view);mSearchView.setQuery(mQuery, false /* submitQuery */);mSearchView.setOnQueryTextListener(this);mSearchView.requestFocus();return view;} Overridepublic boolean onQueryTextChange(String query) {...if (isEmptyQuery) {final LoaderManager loaderManager getLoaderManager();loaderManager.destroyLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT);mShowingSavedQuery true;mSavedQueryController.loadSavedQueries();mSearchFeatureProvider.hideFeedbackButton(getView());} else {mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.PERFORM_SEARCH);restartLoaders();}return true;}Overridepublic boolean onQueryTextSubmit(String query) {// Save submitted query.mSavedQueryController.saveQuery(mQuery);hideKeyboard();return true;}private void restartLoaders() {mShowingSavedQuery false;final LoaderManager loaderManager getLoaderManager();loaderManager.restartLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT,null /* args */, this /* callback */);}Overridepublic LoaderList? extends SearchResult onCreateLoader(int id, Bundle args) {final Activity activity getActivity();switch (id) {case SearchCommon.SearchLoaderId.SEARCH_RESULT:return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery);default:return null;}} }在SearchFeatureProvider实现类SearchFeatureProviderImpl中创建了SearchResultLoader实例SearchResultLoader在子线程进行数据查找。 public class SearchResultLoader extends AsyncLoaderList? extends SearchResult {private final String mQuery;public SearchResultLoader(Context context, String query) {super(context);mQuery query;}Overridepublic List? extends SearchResult loadInBackground() {SearchResultAggregator aggregator SearchResultAggregator.getInstance();return aggregator.fetchResults(getContext(), mQuery);} }fetchResults方法进行数据查找并创建了一个tasks集合然后变量tasks保存到taskResults中。 public class SearchResultAggregator {NonNullpublic synchronized List? extends SearchResult fetchResults(Context context, String query) {final SearchFeatureProvider mFeatureProvider FeatureFactory.get(context).searchFeatureProvider();final ExecutorService executorService mFeatureProvider.getExecutorService();final ListSearchQueryTask tasks mFeatureProvider.getSearchQueryTasks(context, query);// Start tasksfor (SearchQueryTask task : tasks) {executorService.execute(task);}// Collect resultsfinal MapInteger, List? extends SearchResult taskResults new ArrayMap();final long allTasksStart System.currentTimeMillis();for (SearchQueryTask task : tasks) {final int taskId task.getTaskId();try {taskResults.put(taskId,task.get(SHORT_CHECK_TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS));} catch (TimeoutException | InterruptedException | ExecutionException e) {Log.d(TAG, Could not retrieve result in time: taskId, e);taskResults.put(taskId, Collections.EMPTY_LIST);}}// Merge resultsfinal List? extends SearchResult mergedResults mergeSearchResults(taskResults);return mergedResults;} }getSearchQueryTasks中构建了各种类型的task如DatabaseResultTask/InstalledAppResultTask等等。这些task都继承于SearchQueryTask.QueryWorker。 public class SearchFeatureProviderImpl implements SearchFeatureProvider {Overridepublic ListSearchQueryTask getSearchQueryTasks(Context context, String query) {final ListSearchQueryTask tasks new ArrayList();final String cleanQuery cleanQuery(query);tasks.add(DatabaseResultTask.newTask(context, getSiteMapManager(), cleanQuery));tasks.add(InstalledAppResultTask.newTask(context, getSiteMapManager(), cleanQuery));tasks.add(AccessibilityServiceResultTask.newTask(context, getSiteMapManager(), cleanQuery));tasks.add(InputDeviceResultTask.newTask(context, getSiteMapManager(), cleanQuery));return tasks;} }而SearchQueryTask又继承于FutureTaskcall方法去处理任务完成后返回结果。 public class SearchQueryTask extends FutureTaskList? extends SearchResult {public static abstract class QueryWorker implements CallableList? extends SearchResult {Overridepublic List? extends SearchResult call() throws Exception {final long startTime System.currentTimeMillis();try {return query();} finally {final long endTime System.currentTimeMillis();FeatureFactory.get(mContext).metricsFeatureProvider(mContext).logEvent(getQueryWorkerId(), endTime - startTime);}}} }我们以DatabaseResultTask为例查看它实现的query方法。query方法通过一系列的查询方法将数据添加到resultSet中可以看到query方法中获取SQLite数据库实例IndexDatabaseHelper中初始化数据库可以看到数据库名为search_index.db表名和表字段。最后通过query方法查询数据。 public class DatabaseResultTask extends SearchQueryTask.QueryWorker {public static SearchQueryTask newTask(Context context, SiteMapManager siteMapManager,String query) {return new SearchQueryTask(new DatabaseResultTask(context, siteMapManager, query));}Overrideprotected List? extends SearchResult query() {if (mQuery null || mQuery.isEmpty()) {return new ArrayList();}// Start a Future to get search result scores.FutureTaskListPairString, Float rankerTask mFeatureProvider.getRankerTask(mContext, mQuery);if (rankerTask ! null) {ExecutorService executorService mFeatureProvider.getExecutorService();executorService.execute(rankerTask);}final SetSearchResult resultSet new HashSet();resultSet.addAll(firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]));resultSet.addAll(secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1]));resultSet.addAll(anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2]));resultSet.addAll(anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3]));// Try to retrieve the scores in time. Otherwise use static ranking.if (rankerTask ! null) {try {final long timeoutMs mFeatureProvider.smartSearchRankingTimeoutMs(mContext);ListPairString, Float searchRankScores rankerTask.get(timeoutMs,TimeUnit.MILLISECONDS);return getDynamicRankedResults(resultSet, searchRankScores);} catch (TimeoutException | InterruptedException | ExecutionException e) {Log.d(TAG, Error waiting for result scores: e);}}ListSearchResult resultList new ArrayList(resultSet);Collections.sort(resultList);return resultList;}private SetSearchResult firstWordQuery(String[] matchColumns, int baseRank) {final String whereClause buildSingleWordWhereClause(matchColumns);final String query mQuery %;final String[] selection buildSingleWordSelection(query, matchColumns.length);return query(whereClause, selection, baseRank);}private SetSearchResult query(String whereClause, String[] selection, int baseRank) {final SQLiteDatabase database IndexDatabaseHelper.getInstance(mContext).getReadableDatabase();//查询搜索数据try (Cursor resultCursor database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,whereClause,selection, null, null, null)) {return mConverter.convertCursor(resultCursor, baseRank, mSiteMapManager);}} }那么问题来了Settings搜索数据存储在SQLite数据库中我们分析了它的查询流程那么它是如何存储的呢 其实在SearchFragment的onCreate就有实现通过updateIndexAsync刷新数据。 public class SearchFragment extends Fragment implements SearchView.OnQueryTextListener,LoaderManager.LoaderCallbacksList? extends SearchResult, IndexingCallback {...mSearchFeatureProvider.updateIndexAsync(getContext(), this /* indexingCallback */); }通过indexDatabase方法更新数据。 public class SearchFeatureProviderImpl implements SearchFeatureProvider {Overridepublic void updateIndexAsync(Context context, IndexingCallback callback) {if (DEBUG) {Log.d(TAG, updating index async);}getIndexingManager(context).indexDatabase(callback);} }IndexingTask继承于AsyncTask。异步执行performIndexing方法通过queryIntentContentProviders方法获取ContentProvider然后根据provider查找数据更新到数据库中。看下intent指定的action PROVIDER_INTERFACE为android.content.action.SEARCH_INDEXABLES_PROVIDER在Settings查找是否有定义此action的ContentProvider。 public class DatabaseIndexingManager {public void indexDatabase(IndexingCallback callback) {IndexingTask task new IndexingTask(callback);task.execute();}public class IndexingTask extends AsyncTaskVoid, Void, Void {VisibleForTestingIndexingCallback mCallback;private long mIndexStartTime;public IndexingTask(IndexingCallback callback) {mCallback callback;}Overrideprotected void onPreExecute() {mIndexStartTime System.currentTimeMillis();mIsIndexingComplete.set(false);}Overrideprotected Void doInBackground(Void... voids) {performIndexing();return null;}Overrideprotected void onPostExecute(Void aVoid) {int indexingTime (int) (System.currentTimeMillis() - mIndexStartTime);FeatureFactory.get(mContext).metricsFeatureProvider(mContext).logEvent(SettingsIntelligenceLogProto.SettingsIntelligenceEvent.INDEX_SEARCH,indexingTime);mIsIndexingComplete.set(true);if (mCallback ! null) {mCallback.onIndexingFinished();}}}public void performIndexing() {final Intent intent new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);final ListResolveInfo providers mContext.getPackageManager().queryIntentContentProviders(intent, 0);final boolean isFullIndex IndexDatabaseHelper.isFullIndex(mContext, providers);if (isFullIndex) {rebuildDatabase();}PreIndexData indexData getIndexDataFromProviders(providers, isFullIndex);final long updateDatabaseStartTime System.currentTimeMillis();updateDatabase(indexData, isFullIndex);IndexDatabaseHelper.setIndexed(mContext, providers);if (DEBUG) {final long updateDatabaseTime System.currentTimeMillis() - updateDatabaseStartTime;Log.d(TAG, performIndexing updateDatabase took time: updateDatabaseTime);}} }可以发现在Settings的AndroidManifest.xml中指定一个Provider。 providerandroid:name.search.SettingsSearchIndexablesProviderandroid:authoritiescom.android.settingsandroid:multiprocessfalseandroid:grantUriPermissionstrueandroid:permissionandroid.permission.READ_SEARCH_INDEXABLESandroid:exportedtrueintent-filteraction android:nameandroid.content.action.SEARCH_INDEXABLES_PROVIDER //intent-filter/providerSettingsSearchIndexablesProvider继承于SearchIndexablesProviderSearchIndexablesProvider继承于ContentProvider, query方法进行了分类查询插入删除更新均不支持通过final修饰和抛出UnsupportedOperationException屏蔽了。 public abstract class SearchIndexablesProvider extends ContentProvider {Overridepublic Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder) {try {switch (mMatcher.match(uri)) {case MATCH_RES_CODE:return queryXmlResources(null);case MATCH_RAW_CODE:return queryRawData(null);case MATCH_NON_INDEXABLE_KEYS_CODE:return queryNonIndexableKeys(null);case MATCH_SITE_MAP_PAIRS_CODE:return querySiteMapPairs();case MATCH_SLICE_URI_PAIRS_CODE:return querySliceUriPairs();case MATCH_DYNAMIC_RAW_CODE:return queryDynamicRawData(null);default:throw new UnsupportedOperationException(Unknown Uri uri);}} catch (UnsupportedOperationException e) {throw e;} catch (Exception e) {Log.e(TAG, Provider querying exception:, e);return null;}}Overridepublic final Uri insert(Uri uri, ContentValues values) {throw new UnsupportedOperationException(Insert not supported);} }以queryXmlResources为例通过getSearchIndexableResourcesFromProvider方法获取数据集合并保存到cursor中。bundles里一个class类型的集合。 public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider {Overridepublic Cursor queryXmlResources(String[] projection) {final MatrixCursor cursor new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);final ListSearchIndexableResource resources getSearchIndexableResourcesFromProvider(getContext());for (SearchIndexableResource val : resources) {final Object[] ref new Object[INDEXABLES_XML_RES_COLUMNS.length];ref[COLUMN_INDEX_XML_RES_RANK] val.rank;ref[COLUMN_INDEX_XML_RES_RESID] val.xmlResId;ref[COLUMN_INDEX_XML_RES_CLASS_NAME] val.className;ref[COLUMN_INDEX_XML_RES_ICON_RESID] val.iconResId;ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] val.intentAction;ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] val.intentTargetPackage;ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] null; // intent target classcursor.addRow(ref);}return cursor;}private ListSearchIndexableResource getSearchIndexableResourcesFromProvider(Context context) {final CollectionSearchIndexableData bundles FeatureFactory.getFactory(context).getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();ListSearchIndexableResource resourceList new ArrayList();for (SearchIndexableData bundle : bundles) {Indexable.SearchIndexProvider provider bundle.getSearchIndexProvider();final ListSearchIndexableResource resList provider.getXmlResourcesToIndex(context, true);if (resList null) {continue;}for (SearchIndexableResource item : resList) {item.className TextUtils.isEmpty(item.className)? bundle.getTargetClass().getName(): item.className;}resourceList.addAll(resList);}return resourceList;} }SearchIndexableResourcesMobile继承于SearchIndexableResourcesBase public class SearchFeatureProviderImpl implements SearchFeatureProvider {Overridepublic SearchIndexableResources getSearchIndexableResources() {if (mSearchIndexableResources null) {mSearchIndexableResources new SearchIndexableResourcesMobile();}return mSearchIndexableResources;} }SearchIndexableResourcesMobile类生成在IndexableProcessor中IndexableProcessor设置的注解为SearchIndexableSearchIndexable注解可以指定target(ALL/MOBILE/TV/WEAR/AUTO/ARC)对应不同平台。通过JavaPoet库来addCode实例化SearchIndexableDatagetProviderValues方法返回的是带有SearchIndexable注解的所有类集合。 SupportedSourceVersion(SourceVersion.RELEASE_9) SupportedAnnotationTypes({com.android.settingslib.search.SearchIndexable}) public class IndexableProcessor extends AbstractProcessor {Overridepublic boolean process(Set? extends TypeElement annotations,for (Element element : roundEnvironment.getElementsAnnotatedWith(SearchIndexable.class)) {if (element.getKind().isClass()) {Name className element.accept(new SimpleElementVisitor8Name, Void() {Overridepublic Name visitType(TypeElement typeElement, Void aVoid) {return typeElement.getQualifiedName();}}, null);if (className ! null) {SearchIndexable searchIndexable element.getAnnotation(SearchIndexable.class);int forTarget searchIndexable.forTarget();MethodSpec.Builder builder baseConstructorBuilder;if (forTarget SearchIndexable.ALL) {builder baseConstructorBuilder;} else if ((forTarget SearchIndexable.MOBILE) ! 0) {builder mobileConstructorBuilder;} else if ((forTarget SearchIndexable.TV) ! 0) {builder tvConstructorBuilder;} else if ((forTarget SearchIndexable.WEAR) ! 0) {builder wearConstructorBuilder;} else if ((forTarget SearchIndexable.AUTO) ! 0) {builder autoConstructorBuilder;} else if ((forTarget SearchIndexable.ARC) ! 0) {builder arcConstructorBuilder;}//实例化SearchIndexableDatabuilder.addCode($N(new SearchIndexableData($L.class, $L .SEARCH_INDEX_DATA_PROVIDER));\n,addIndex, className, className);...}}inal MethodSpec getProviderValues MethodSpec.methodBuilder(getProviderValues).addAnnotation(Override.class).addModifiers(Modifier.PUBLIC).returns(ParameterizedTypeName.get(ClassName.get(Collection.class),searchIndexableData)).addCode(return $N;\n, providers).build();final TypeSpec baseClass TypeSpec.classBuilder(CLASS_BASE).addModifiers(Modifier.PUBLIC).addSuperinterface(ClassName.get(PACKAGE, SearchIndexableResources)).addField(providers).addMethod(baseConstructorBuilder.build()).addMethod(addIndex).addMethod(getProviderValues).build();final JavaFile searchIndexableResourcesBase JavaFile.builder(PACKAGE, baseClass).build();final JavaFile searchIndexableResourcesMobile JavaFile.builder(PACKAGE,TypeSpec.classBuilder(CLASS_MOBILE).addModifiers(Modifier.PUBLIC).superclass(ClassName.get(PACKAGE, baseClass.name)).addMethod(mobileConstructorBuilder.build()).build()).build(); }实例化SearchIndexableDatamTargetClass为className.classmSearchIndexProvider为className.SEARCH_INDEX_DATA_PROVIDER其中的className就是对应添加SearchIndexable注解的类名 public class SearchIndexableData {public SearchIndexableData(Class targetClass, Indexable.SearchIndexProvider provider) {mTargetClass targetClass;mSearchIndexProvider provider;} }总结一下Settings搜索功能就是在需要被提供的页面添加SearchIndexable注解在这页面创建一个常量SEARCH_INDEX_DATA_PROVIDER这个常量类型必须为Indexable.SearchIndexProvider。以TopLevelSettings为例。添加了SearchIndexable注解指定Target为MOBILE也创建了SEARCH_INDEX_DATA_PROVIDERSettings封装了一个基础的SearchIndexProvider不返回任何要索引的数据类名为BaseSearchIndexProvider。 SearchIndexable(forTarget MOBILE) public class TopLevelSettings extends DashboardFragment implements SplitLayoutListener,PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER new BaseSearchIndexProvider(R.xml.top_level_settings) {Overrideprotected boolean isPageSearchEnabled(Context context) {// Never searchable, all entries in this page are already indexed elsewhere.return false;}}; }SearchIndexProvider和BaseSearchIndexProvider扩展的方法可以让我们准确处理菜单搜索需求。 public class BaseSearchIndexProvider implements Indexable.SearchIndexProvider {public BaseSearchIndexProvider() {}public BaseSearchIndexProvider(int xmlRes) {mXmlRes xmlRes;}//返回SearchIndexableResourceOverridepublic ListSearchIndexableResource getXmlResourcesToIndex(Context context, boolean enabled) {if (mXmlRes ! 0) {final SearchIndexableResource sir new SearchIndexableResource(context);sir.xmlResId mXmlRes;return Arrays.asList(sir);}return null;}//返回SearchIndexableRaw集合Overridepublic ListSearchIndexableRaw getRawDataToIndex(Context context, boolean enabled) {return null;}//返回动态数据集合Overridepublic ListSearchIndexableRaw getRawDataToIndex(Context context, boolean enabled) {return null;}//无法搜索的集合OverrideCallSuperpublic ListString getNonIndexableKeys(Context context) {...}//页面是否启用搜索protected boolean isPageSearchEnabled(Context context) {return true;}//获取xml设置禁用搜索的集合VisibleForTesting(otherwise VisibleForTesting.PROTECTED)public ListString getNonIndexableKeysFromXml(Context context, XmlRes int xmlResId,boolean suppressAllPage) {return getKeysFromXml(context, xmlResId, suppressAllPage);} }以上就是Settings的搜索逻辑。要测试新菜单的可搜索性需要先清除Settings数据让数据库重新添加数据。 总结 Settings菜单如果想要支持搜索首先对应页面需要添加SearchIndexable注解其次在本页面创建一个常量SEARCH_INDEX_DATA_PROVIDER然后根据需要重写需要的实现方法。这样这个菜单就支持搜索了。 SettingsIntelligence会扫描这些添加SearchIndexable注解的页面将这些页面的菜单添加到数据库中查询时根据关键词进行匹配查询。
http://www.dnsts.com.cn/news/231324.html

相关文章:

  • 专业医院网站建设网站SEO做点提升流量万象
  • 网站包503错误专业建设规划及实施方案
  • 建设银行网站进不去夏门建设局网站
  • 做海淘网站赚钱吗中国建设银行官网站电话号码
  • 广告设计自学网教程重庆seo1
  • 魔兽世界 建设公会网站网站建设系统认证系统
  • 上海网站建设价格表重庆市建设工程施工安全网
  • c 网站开发数据库连接哈尔滨做网站哪里好
  • 有什么外贸网站网站建设询价报告
  • 哪里有网站推广优化360平台怎么做网站优化
  • 网站服务器端口号是什么免费网站外链推广
  • 奥派电子商务网站建设论文电商平台市场调研报告
  • 制作网站的网站企业网站搭建多少钱
  • 网站单页宁波产城生态建设集团网站
  • 深圳外贸网站建设服务哪家好网络推广营销方案100例
  • 专业的网站设计网络页面设计介绍
  • phpcms 网站源码天元建设集团有限公司企查查
  • 广州h5网站本科自考有哪些科目
  • 做动图素材网站html静态页面怎么放在网站上
  • 网站开发看书网站建设费要摊销
  • 梧州高端网站建设服务新媒体营销的概念是什么
  • 黄岛区网站建设网络营销课程介绍
  • 可以自己做网站服务器不网站前置审批流程
  • 哪个旅游网站做的最好江苏seo培训
  • 新建网站需要多少钱邯郸网站设计开发公司
  • 免费做文字图网站网站怎么加关键词
  • 昆明网站服务器佳木斯 网站建设
  • 企业网站建设解决方案宁波造价工程信息网
  • 公司一定建设网站吗做毕设网站多少钱
  • 阜宁做网站wordpress特色缩略图