曾大稳丶


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于

Android setContentView()源码流程分析

发表于 2018-03-17 | 分类于 源码分析 |
字数统计: 3,793字 | 阅读时长 ≈ 22分钟

我们在Activity创建的时候,都用调用setContentView()函数来设置界面,下面我们通过源码来分析setContentView()的流程。
我们先看Activity里面的setContentView进去查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

函数如下:
我们再次点击setContentView进去查看发现是一个Window的抽象方法,所以我们要找到对应的实现类,我们点击getWindow()进去查看最终得到:

1
mWindow = new PhoneWindow(this, window);

我们得到了他的实现类PhoneWindow,然后在PhoneWindow找到setContentView函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}

@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}

可以发现都是把这个View加到了mContentParent这个ViewGroup里面去了,这个mContentParent是什么呢?我们急着看源码,发现在installDecor()方法里面:

1
mContentParent = generateLayout(mDecor);

继续点进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.

TypedArray a = getWindowStyle();

if (false) {
System.out.println("From style:");
String s = "Attrs:";
for (int i = 0; i < R.styleable.Window.length; i++) {
s = s + " " + Integer.toHexString(R.styleable.Window[i
![无标题.png](http://upload-images.jianshu.io/upload_images/4658633-9a3a758cbac9f8aa.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
]) + "="
+ a.getString(i);
}
System.out.println(s);
}

//根据style属性做一些列的判断...

//在做一些列的判断得到layoutResource
layoutResource=.... //这里用R.layout.screen_simple来分析

mDecor.startChanging();

View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}

//......

mDecor.finishChanging();

return contentParent;
}

从这段代码可以知道,通过一系列的判断,得到相对于的layoutResource,然后通过mLayoutInflater.inflate(layoutResource, null);得到这个View,将其加入到mDecor,其中mContentParent最终为一个ID_ANDROID_CONTENT = com.android.internal.R.id.content的一个ViewGroup,在这里我们拿R.layout.screen_simple布局来看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?xml version="1.0" encoding="utf-8"?>
<!--
/* //device/apps/common/assets/res/layout/screen_simple.xml
**
** Copyright 2006, 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.
*/

This is an optimized layout for a screen, with the minimum set of features
enabled.
-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

可以知道这个mContentParent为一个FrameLayout,这时候我们应该知道,我们setContentView其实就是把那个View加入到一个id为com.android.internal.R.id.content的FrameLayout里面,而这个id为com.android.internal.R.id.content的FrameLayout有parentView又是加在mDecor里面,我们来看看这个mDecor是什么,在installDecor()函数中:

1
2
3
4
5
6
7
8
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}

1
2
3
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}

可以发现这个mDecor就是一个new的一个DecorView,我们继续看:

1
private final class DecorView extends FrameLayout

这个DecorView其实也是一个FrameLayout,这个时候我们就可以得到这样一张图:

在这基础上我们看AppCompatActivity的setContentView函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  @Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}


/**
* @return The {@link AppCompatDelegate} being used by this Activity.
*/
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}

/**
* Create a {@link android.support.v7.app.AppCompatDelegate} to use with {@code activity}.
*
* @param callback An optional callback for AppCompat specific events
*/
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return create(activity, activity.getWindow(), callback);
}
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV7(context, window, callback);
}
}

可以看到最终进入到了AppCompatDelegate的create方法,这个函数通过new 23 14 11 7就可以看出是为了兼容不同的版本,我们点进去就可以看到AppCompatDelegateImplV23–>AppCompatDelegateImplV14–>AppCompatDelegateImplV11–>AppCompatDelegateImplV7依次继承的,我们最终查看到AppCompatDelegateImplV7的setContentView函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public void setContentView(View v) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v);
mOriginalWindowCallback.onContentChanged();
}

@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}

@Override
public void setContentView(View v, ViewGroup.LayoutParams lp) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v, lp);
mOriginalWindowCallback.onContentChanged();
}

其实也就是把得到的view添加到contentParent里面。
比如说,在android21以前一般是使用控件TextView等控件,在21以后出了相关的AppCompat控件,这个时候怎么让开发者写的TextView自动转换为AppCompatTextView呢?所以在AppCompatDelegateImplV7重写了这样一个函数函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
if (!(LayoutInflaterCompat.getFactory(layoutInflater)
instanceof AppCompatDelegateImplV7)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}

我们查找abstract类,可以查看着方法注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Installs AppCompat's {@link android.view.LayoutInflater} Factory so that it can replace
* the framework widgets with compatible tinted versions. This should be called before
* {@code super.onCreate()} as so:
* <pre class="prettyprint">
* protected void onCreate(Bundle savedInstanceState) {
* getDelegate().installViewFactory();
* getDelegate().onCreate(savedInstanceState);
* super.onCreate(savedInstanceState);
*
* // ...
* }
* </pre>
* If you are using your own {@link android.view.LayoutInflater.Factory Factory} or
* {@link android.view.LayoutInflater.Factory2 Factory2} then you can omit this call, and instead call
* {@link #createView(android.view.View, String, android.content.Context, android.util.AttributeSet)}
* from your factory to return any compatible widgets.
*/
public abstract void installViewFactory();

从意思可以看出,就是说我们可以通过这个方法然后给LayoutInflater设置一个Factory,这个Factory是干嘛的呢?从我的上篇文章就知道,这个Factory是在LayoutInflater执行inflate函数生成View的时候用的,这个Factory可以拦截View的生成,通过这个Factory我们可以自己给inflate写一套解析layout.xml的规则,在换肤的时候就可以用到这个。我们实现LayoutInflaterFactory接口,重写onCreateView方法,就可以拦截相应的信息进行解析。比如在AppCompatDelegateImplV7类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 /**
* From {@link android.support.v4.view.LayoutInflaterFactory}
*/
@Override
public final View onCreateView(View parent, String name,
Context context, AttributeSet attrs) {
// First let the Activity's Factory try and inflate the view
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}

// If the Factory didn't handle it, let our createView() method try
return createView(parent, name, context, attrs);
}


View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
// Let the Activity's LayoutInflater.Factory try and handle it
if (mOriginalWindowCallback instanceof LayoutInflater.Factory) {
final View result = ((LayoutInflater.Factory) mOriginalWindowCallback)
.onCreateView(name, context, attrs);
if (result != null) {
return result;
}
}
return null;
}

@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
final boolean isPre21 = Build.VERSION.SDK_INT < 21;

if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}

// We only want the View to inherit its context if we're running pre-v21
final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);

return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
true /* Read read app:theme as a fallback at all times for legacy reasons */
);
}

它拦截下了layoutt.xml的解析,自己写了一个解析类AppCompatViewInflater,来解析View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
/*
* Copyright (C) 2014 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 android.support.v7.app;

import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.TypedArray;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.ArrayMap;
import android.support.v4.view.ViewCompat;
import android.support.v7.appcompat.R;
import android.support.v7.view.ContextThemeWrapper;
import android.support.v7.widget.AppCompatAutoCompleteTextView;
import android.support.v7.widget.AppCompatButton;
import android.support.v7.widget.AppCompatCheckBox;
import android.support.v7.widget.AppCompatCheckedTextView;
import android.support.v7.widget.AppCompatEditText;
import android.support.v7.widget.AppCompatImageButton;
import android.support.v7.widget.AppCompatImageView;
import android.support.v7.widget.AppCompatMultiAutoCompleteTextView;
import android.support.v7.widget.AppCompatRadioButton;
import android.support.v7.widget.AppCompatRatingBar;
import android.support.v7.widget.AppCompatSeekBar;
import android.support.v7.widget.AppCompatSpinner;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.InflateException;
import android.view.View;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;

/**
* This class is responsible for manually inflating our tinted widgets which are used on devices
* running {@link android.os.Build.VERSION_CODES#KITKAT KITKAT} or below. As such, this class
* should only be used when running on those devices.
* <p>This class two main responsibilities: the first is to 'inject' our tinted views in place of
* the framework versions in layout inflation; the second is backport the {@code android:theme}
* functionality for any inflated widgets. This include theme inheritance from it's parent.
*/
class AppCompatViewInflater {

private static final Class<?>[] sConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
private static final int[] sOnClickAttrs = new int[]{android.R.attr.onClick};

private static final String[] sClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};

private static final String LOG_TAG = "AppCompatViewInflater";

private static final Map<String, Constructor<? extends View>> sConstructorMap
= new ArrayMap<>();

private final Object[] mConstructorArgs = new Object[2];

public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme) {
final Context originalContext = context;

// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
// by using the parent's context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
// We then apply the theme on the context, if specified
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}

View view = null;

// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}

if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}

if (view != null) {
// If we have created a view, check it's android:onClick
checkOnClickListener(view, attrs);
}

return view;
}

private View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}

try {
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;

if (-1 == name.indexOf('.')) {
for (int i = 0; i < sClassPrefixList.length; i++) {
final View view = createView(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {
return createView(context, name, null);
}
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
} finally {
// Don't retain references on context.
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}

/**
* android:onClick doesn't handle views with a ContextWrapper context. This method
* backports new framework functionality to traverse the Context wrappers to find a
* suitable target.
*/
private void checkOnClickListener(View view, AttributeSet attrs) {
final Context context = view.getContext();

if (!(context instanceof ContextWrapper) ||
(Build.VERSION.SDK_INT >= 15 && !ViewCompat.hasOnClickListeners(view))) {
// Skip our compat functionality if: the Context isn't a ContextWrapper, or
// the view doesn't have an OnClickListener (we can only rely on this on API 15+ so
// always use our compat code on older devices)
return;
}

final TypedArray a = context.obtainStyledAttributes(attrs, sOnClickAttrs);
final String handlerName = a.getString(0);
if (handlerName != null) {
view.setOnClickListener(new DeclaredOnClickListener(view, handlerName));
}
a.recycle();
}

private View createView(Context context, String name, String prefix)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);

try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
Class<? extends View> clazz = context.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);

constructor = clazz.getConstructor(sConstructorSignature);
sConstructorMap.put(name, constructor);
}
constructor.setAccessible(true);
return constructor.newInstance(mConstructorArgs);
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
}
}

/**
* Allows us to emulate the {@code android:theme} attribute for devices before L.
*/
private static Context themifyContext(Context context, AttributeSet attrs,
boolean useAndroidTheme, boolean useAppTheme) {
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.View, 0, 0);
int themeId = 0;
if (useAndroidTheme) {
// First try reading android:theme if enabled
themeId = a.getResourceId(R.styleable.View_android_theme, 0);
}
if (useAppTheme && themeId == 0) {
// ...if that didn't work, try reading app:theme (for legacy reasons) if enabled
themeId = a.getResourceId(R.styleable.View_theme, 0);

if (themeId != 0) {
Log.i(LOG_TAG, "app:theme is now deprecated. "
+ "Please move to using android:theme instead.");
}
}
a.recycle();

if (themeId != 0 && (!(context instanceof ContextThemeWrapper)
|| ((ContextThemeWrapper) context).getThemeResId() != themeId)) {
// If the context isn't a ContextThemeWrapper, or it is but does not have
// the same theme as we need, wrap it in a new wrapper
context = new ContextThemeWrapper(context, themeId);
}
return context;
}

/**
* An implementation of OnClickListener that attempts to lazily load a
* named click handling method from a parent or ancestor context.
*/
private static class DeclaredOnClickListener implements View.OnClickListener {
private final View mHostView;
private final String mMethodName;

private Method mResolvedMethod;
private Context mResolvedContext;

public DeclaredOnClickListener(@NonNull View hostView, @NonNull String methodName) {
mHostView = hostView;
mMethodName = methodName;
}

@Override
public void onClick(@NonNull View v) {
if (mResolvedMethod == null) {
resolveMethod(mHostView.getContext(), mMethodName);
}

try {
mResolvedMethod.invoke(mResolvedContext, v);
} catch (IllegalAccessException e) {
throw new IllegalStateException(
"Could not execute non-public method for android:onClick", e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(
"Could not execute method for android:onClick", e);
}
}

@NonNull
private void resolveMethod(@Nullable Context context, @NonNull String name) {
while (context != null) {
try {
if (!context.isRestricted()) {
final Method method = context.getClass().getMethod(mMethodName, View.class);
if (method != null) {
mResolvedMethod = method;
mResolvedContext = context;
return;
}
}
} catch (NoSuchMethodException e) {
// Failed to find method, keep searching up the hierarchy.
}

if (context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext();
} else {
// Can't search up the hierarchy, null out and fail.
context = null;
}
}

final int id = mHostView.getId();
final String idText = id == View.NO_ID ? "" : " with id '"
+ mHostView.getContext().getResources().getResourceEntryName(id) + "'";
throw new IllegalStateException("Could not find method " + mMethodName
+ "(View) in a parent or ancestor Context for android:onClick "
+ "attribute defined on view " + mHostView.getClass() + idText);
}
}
}

这样就达到了将以前的TextView等转换为相关的AppCompat控件,达到兼容。

setContentView()源码流程就分析到这里,细看请自行查看源码。

Android LayoutInflater.inflate()源码流程分析

发表于 2018-03-16 | 分类于 源码分析 |
字数统计: 2,488字 | 阅读时长 ≈ 14分钟

  我们在根据layout文件得到View的时候都会使用LayoutInflater.from(mContext).inflate().下面我们来分析这个获取View流程。
  我们知道inflate有如下函数:

1
2
3
inflate(@LayoutRes int resource, @Nullable ViewGroup root);
inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot);
inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot);

其实点进去查看可以知道,其实都到了这个方法:

1
inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot);

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
* Inflate a new view hierarchy from the specified XML node. Throws
* {@link InflateException} if there is an error.
* <p>
* <em><strong>Important</strong></em>   For performance
* reasons, view inflation relies heavily on pre-processing of XML files
* that is done at build time. Therefore, it is not currently possible to
* use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
*
* @param parser XML dom node containing the description of the view
* hierarchy.
* @param root Optional view to be the parent of the generated hierarchy (if
* <em>attachToRoot</em> is true), or else simply an object that
* provides a set of LayoutParams values for root of the returned
* hierarchy (if <em>attachToRoot</em> is false.)
* @param attachToRoot Whether the inflated hierarchy should be attached to
* the root parameter? If false, root is only used to create the
* correct subclass of LayoutParams for the root view in the XML.
* @return The root View of the inflated hierarchy. If root was supplied and
* attachToRoot is true, this is root; otherwise it is the root of
* the inflated XML file.
*/
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;

try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}

if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}

final String name = parser.getName();

if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}

if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}

rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);

ViewGroup.LayoutParams params = null;

if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}

if (DEBUG) {
System.out.println("-----> start inflating children");
}

// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);

if (DEBUG) {
System.out.println("-----> done inflating children");
}

// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}

// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}

} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;

Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

return result;
}
}

其实就是通过XmlPullParser来解析layout.xml布局
在这里通过判断,如果是merge标签就会执行如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Recursive method used to descend down the xml hierarchy and instantiate
* views, instantiate their children, and then call onFinishInflate().
* <p>
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* override it.
*/
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

final int depth = parser.getDepth();
int type;

while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

if (type != XmlPullParser.START_TAG) {
continue;
}

final String name = parser.getName();

if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}

if (finishInflate) {
parent.onFinishInflate();
}
}

我这主要讲根据layout.xml获取里面的View流程,这些不同的标签不同的解析方式,在这不进行细讲,有趣的可以自行查看源码。在这我们重点看createViewFromTag函数,因为在如果layout开始标签不是merge的话也会调用这个函数创建View,可以发现最终进入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* Creates a view from a tag name using the supplied attribute set.
* <p>
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* override it.
*
* @param parent the parent view, used to inflate layout params
* @param name the name of the XML tag used to define the view
* @param context the inflation context for the view, typically the
* {@code parent} or base layout inflater context
* @param attrs the attribute set for the XML tag used to define the view
* @param ignoreThemeAttr {@code true} to ignore the {@code android:theme}
* attribute (if set) for the view being inflated,
* {@code false} otherwise
*/
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}

// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}

if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}

try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}

if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}

if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}

return view;
} catch (InflateException e) {
throw e;

} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;

} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}

通过这段函数可以知道到View是在这里创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}

if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}

if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}

return view;

可以发现View创建经过了实际是由mFactory2–>Factory–>mPrivateFactory的判断才进入到了onCreateView方法,在这里我们就可以知道,如果我们要拦截View的创建,我们就可以给LayoutInflater设置一个我们自定义的一个Factory即可,并且创建View的规则我们在自己的Factory类中实现即可。具体的实现方式点此查看,我们继续看onCreateView(parent, name, attrs);
如果是-1 == name.indexOf('.')的,即不是自定义的View将会执行:

1
createView(name, "android.view.", attrs);

是自定义的则是:

1
view = createView(name, null, attrs);

createView(String name, String prefix, AttributeSet attrs)函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* Low-level function for instantiating a view by name. This attempts to
* instantiate a view class of the given <var>name</var> found in this
* LayoutInflater's ClassLoader.
*
* <p>
* There are two things that can happen in an error case: either the
* exception describing the error will be thrown, or a null will be
* returned. You must deal with both possibilities -- the former will happen
* the first time createView() is called for a class of a particular name,
* the latter every time there-after for that class name.
*
* @param name The full name of the class to be instantiated.
* @param attrs The XML attributes supplied for this instance.
*
* @return View The newly instantiated view, or null.
*/
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;

try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);

if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);

boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}

Object[] args = mConstructorArgs;
args[1] = attrs;

final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;

} catch (NoSuchMethodException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;

} catch (ClassCastException e) {
// If loaded class is not a View subclass
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
final InflateException ie = new InflateException(
attrs.getPositionDescription() + ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

可以知道其实就是通过反射来进行View的创建,先在constructor缓存集合sConstructorMap里面查找对应的View的constructor进行初始化,如果没有就通过反射拿到constructor,然后缓存到sConstructorMap里面,创建View的时候是通过prefix拼接的,如果xml里面是TextView则拼接出来则是android.view.TextView,所以上面不是自定的View的话,执行的是createView(name, "android.view.", attrs);,如果是自定义的话就不用拼接了。可以发现通过代码

1
2
3
4
 constructor = clazz.getConstructor(mConstructorSignature);

static final Class<?>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};

得知,最终创建View的构造方法是带有Context和AttributeSet参数的方法,这也就解析了我们在自定义一些View的时候,如果不重写这个带有这两个参数的方法的话将会崩溃的现象。

LayoutInflater.inflate()源码流程分析就到这里了。因为在这里读的是流程,所有很多细节的东西还是希望大家自己去源码中看看。

Android View的Touch事件分发

发表于 2018-03-15 | 分类于 源码分析 |
字数统计: 2,094字 | 阅读时长 ≈ 11分钟

事件分发的重要性我就不多说了,我们先从简到难。
先看View的Touch事件分发,我自定义一个View,重写OnTouchEvent函数,然后分别设置OnTouchListener和OnClick:

自定义重写OnTouchEvent

布局

设置onTouchListener和onClick

ACTION_DOWN = 0 ACTION_UP=1 ACTION_MOVE=2
我们我们按下这个View点击一下:

点击

可以发现执行的顺序是:
OnTouchListener.DOWN -> OnTouchEvent.DOWN -> OnTouchListener.MOVE -> OnTouchEvent.MOVE -> OnTouchListener.UP-> OnTouchEvent.UP-> OnClickListener
从这我们就可以猜想执行的优先级为
OnTouchListener > onTouchEvent > onClick
接下来我们验证这个猜想,
我们把OnTouchListener的onTouch返回值改为true
OnTouchListener的onTouch返回值改为true
我在点击一下,这里大胆猜想一下onTouchEvent和onClick不会执行了,看看执行的顺序

OnTouchListener的onTouch返回值改为true之后的执行顺序
这时候执行的顺序如下:
OnTouchListener.DOWN ->OnTouchListener.MOVE-> OnTouchListener.UP
这里验证了我的猜想,可以得到如下结论

View的Touch事件分发,OnToucherListener如果返回true的话,就说明把事件从OnToucherListener这里拦截了,后续的onTouchEvent和onClick就收不到事件了。

接下来我们把OnTouchListener的onTouch返回值改为false,让它不拦截事件,把onTouchEvent返回值改为true
onTouchEvent返回值改为true

OnTouchListener的onTouch返回值改为false
我们点击一下,猜想是OnTouchListener和onTouchEvent能够接收到事件,onClick将不会触发

和我们想的一致,这时候执行顺序变为:
OnTouchListener.DOWN ->OnTouchEvent.DOWN-> OnTouchListener.MOVE -> OnTouchEvent.MOVE->OnTouchListener.UP ->OnTouchEvent.UP
这里我们就可能得到结论

View的Touch事件分发,如果OnToucherListener返回false,onTouchEvent返回true,就说明把事件从onTouchEvent这里拦截了,onClick就不会触发。

通过上面两个结论我们验证了我们的优先级猜想

View的Touch事件分发,执行的优先级为OnTouchListener > onTouchEvent > onClick,如果前两个任意一个地方返回true,那么后续将不会收到事件。

接下来我们从源码的角度分析,首先我们需要知道,你点击或者或者触摸任何一个View 都会调用 dispatchTouchEvent()函数,我们就从这里开始分析源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}

boolean result = false;

if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}

if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

if (!result && onTouchEvent(event)) {
result = true;
}
}

if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}

// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}

return result;
}

我们先要知道ListenerInfo这个是做什么的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static class ListenerInfo {
/**
* Listener used to dispatch focus change events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnFocusChangeListener mOnFocusChangeListener;
/**
* Listeners for layout change events.
*/
private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;
protected OnScrollChangeListener mOnScrollChangeListener;
/**
* Listeners for attach events.
*/
private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;
/**
* Listener used to dispatch click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
public OnClickListener mOnClickListener;
/**
* Listener used to dispatch long click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnLongClickListener mOnLongClickListener;
/**
* Listener used to dispatch context click events. This field should be made private, so it
* is hidden from the SDK.
* {@hide}
*/
protected OnContextClickListener mOnContextClickListener;
/**
* Listener used to build the context menu.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnCreateContextMenuListener mOnCreateContextMenuListener;
private OnKeyListener mOnKeyListener;
private OnTouchListener mOnTouchListener;
private OnHoverListener mOnHoverListener;
private OnGenericMotionListener mOnGenericMotionListener;
private OnDragListener mOnDragListener;
private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;
OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
}

这是一个view所有事件的集合类。接下来进入这段代码,

1
2
3
4
5
6
7
8
9
10
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

if (!result && onTouchEvent(event)) {
result = true;
}

从这段代码我们就可以知道如果mOnTouchListener !=null并且当前view的是enable=true就会执行li.mOnTouchListener.onTouch(this, event),执行li.mOnTouchListener.onTouch(this, event)返回的false的话就会执行onTouchEvent(event)。
从这我们就可以知道OnTouchListener的优先级大于onTouchEvent。接着我们点击onTouchEvent进入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
public boolean onTouchEvent(MotionEvent event) {
//......代码太长 省略
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}

if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();

// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}

if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}

if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}

removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;

case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;

if (performButtonActionOnTouchDown(event)) {
break;
}

// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();

// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;

case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;

case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);

// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();

setPressed(false);
}
}
break;
}

return true;
}

return false;
}

可以看到,我们在MotionEvent.ACTION_UP事件里面,经过一系列的判断,然后进入到了performClick()这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}

sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}

这个函数很明显的就知道是执行onClick,从这就可以得到如下结论

onClick事件是在onTouchEvent的MotionEvent.ACTION_UP事件通过performClick()->li.mOnClickListener.onClick(this)触发的。

到这里我们就验证了我们刚才的优先级的结论。当然在onTouchEvent(MotionEvent event源码中,我们在MotionEvent.ACTION_DOWN里面可以看到长按事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
      case MotionEvent.ACTION_DOWN:
//...
checkForLongClick(0, x, y);
break;

//检测长按事件
private void checkForLongClick(int delayOffset, float x, float y) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
mHasPerformedLongPress = false;

if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}

private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;

@Override
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
//触发长按事件
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}

public void setAnchor(float x, float y) {
mX = x;
mY = y;
}

public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
}



public boolean performLongClick(float x, float y) {
mLongClickX = x;
mLongClickY = y;
final boolean handled = performLongClick();
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}

public boolean performLongClick() {
return performLongClickInternal(mLongClickX, mLongClickY);
}

private boolean performLongClickInternal(float x, float y) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

boolean handled = false;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
if (!handled) {
final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}

从这段代码我们又可以得到如下结论

View的OnLongClickListener是在onTouchEvent的MotionEvent.ACTION_DOWN事件通过checkForLongClick() ->performLongClick(mX, mY)->performLongClick() ->performLongClickInternal(mLongClickX, mLongClickY) ->li.mOnLongClickListener.onLongClick(View.this)的执行顺序触发的。

这样View的OnTouch事件分发机制就分析得差不多,具体的判断细节等还是需要自己查看源码。

参考链接:
http://www.jianshu.com/p/98d1895c409d
http://www.jianshu.com/p/e99b5e8bd67b

Android ViewGroup事件分发

发表于 2018-03-15 | 分类于 源码分析 |
字数统计: 2,115字 | 阅读时长 ≈ 10分钟

上篇文章已经分析了Android的Touch事件分发。如果没看的建议先看一下。Android View的Touch事件分发。
接下来我们开始写几种场景,得出一个初步的执行顺序,然后我们按照这个顺序开始分析。


首先我们自定义一个ViewGroup和一个View,然后重写相关事件进行打印:

场景一:正常返回super,TouchView设置click和onTouchListener事件(onTouch返回false)
TouchViewGroup.png

TouchView.png

布局.png

TouchView设置事件.png

这时候我们点击一下TouchView,触发事件:

点击一下.png

可以看到触发的DOWN MOVE UP事件顺序都为:
ViewGroup.dispatchTouchEvent -> ViewGroup.onInterceptTouchEvent -> View.dispatchTouchEvent -> View.onTouch -> View.onTouchEven
只是在UP事件的时候最后多了一个click事件。


场景二:在场景一的基础上取消TouchView的onClick事件

TouchView取消click事件.png

这时候发现除了,执行的顺序变为了:
ViewGroup.dispatchTouchEvent -> ViewGroup.onInterceptTouchEvent -> View.dispatchTouchEvent -> View.onTouch -> View.onTouchEven->ViewGroup.onTouchEven
并且只有DOWN事件,其他事件就没有了。


场景三:在场景二的基础上TouchViewGroup的onInterceptTouchEvent里面返回true


这个时候就只有DOWN事件,并且顺序为:
ViewGroup.dispatchTouchEvent -> ViewGroup.onInterceptTouchEvent -> ViewGroup.onTouchEvent


接下来我们通过源码来分析:
首先从ViewGroup的dispatchTouchEvent入手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
 @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//...
boolean handled = false;
//...


//1.取消之前的手势
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}

//2.判断是否拦截
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { //DOWN
//父类是否拦截 getParent().requestDisallowInterceptTouchEvent();来改变值
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}

//....
//3.0 如果是不取消不拦截为down,并且dispatchTransformedTouchEvent返回为true的时候会为 mFirstTouchTarget赋值
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//3.1 如果不取消并且不拦截的情况下,
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {// 3.2 DOWN的时候
//...
if (newTouchTarget == null && childrenCount != 0) {
//...
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {//3.3 反序for循环,为了先拿到上层的view
//...
//3.4 拿到child
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
//...
//3.5 根据child给newTouchTarget赋值 DOWN的时候因为 mFirstTouchTarget==null 所以进不去 返回的是null
newTouchTarget = getTouchTarget(child);
}
//...
//3.6. 执行操作 是执行自己的dispatchTouchEvent还是child的dispatchTouchEvent
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

//...
//3.7 子View如果返回true添加一个newTouchTarget 并且为mFirstTouchTarget赋值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//....
}
}
}
}
//...
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {//执行自身的dispatchTouchEvent
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {// mFirstTouchTarget已经赋值
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {//执行完3.7操作的
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}



return handled;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
* Cancels and clears all touch targets.
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}

for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();

if (syntheticEvent) {
event.recycle();
}
}
}

//清楚所有的TouchTarget
/**
* Clears all touch targets.
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}


//根据childVie得到TouchTarget
/**
* Gets the touch target for specified child view.
* Returns null if not found.
*/
private TouchTarget getTouchTarget(@NonNull View child) {
// DOWN的时候因为 mFirstTouchTarget==null 所以进不去 返回的是null
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}




/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
//伪代码
final boolean handled;
if (child == null) {//执行View.dispatchTouchEvent 也就是自己的dispatchTouchEvent
handled = super.dispatchTouchEvent(event);
} else {//执行child的dispatchTouchEvent
handled = child.dispatchTouchEvent(event);
}
return handled;
}



//添加TouchTarget 并且给mFirstTouchTarget赋值
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
  1. 当DOWN的时候,从注释和方法名可以看出,会调用cancelAndClearTouchTargets,然后在调用clearTouchTargets使mFirstTouchTarget = null用来废弃上一次的触摸手势。
  2. 接着判断父类需不需要拦截,先通过(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0来判断,在这里可以通过getParent().requestDisallowInterceptTouchEvent(boolean disallowIntercept)来改变值,如果上面为判断为false再通过onInterceptTouchEvent的返回值来确定,这个函数默认情况下返回false。
  3. 检测是否为取消事件,如果不是取消、不拦截并且为 DOWN事件的时候,就会对childView一个反序的for循环来遍历,并且执行dispatchTransformedTouchEvent操作,这个操作用来执行dispatchTouchEvent,如果childView是null的话将执行View.dispatchTouchEvent,也就是自己的dispatchTouchEvent,反之执行childView的dispatchTouchEvent,如果执行dispatchTransformedTouchEvent返回的值是true那么将会调用addTouchTarget()为这个childView生成一个TouchTarget并且执行mFirstTouchTarget = target将之赋值于mFirstTouchTarget ,然后跳出for循环遍历,这个mFirstTouchTarget是用于判断后续的事件move up等事件是否进行拦截触发函数。
  4. 判断操作,首先判断mFirstTouchTarget是否为null,如果是DOWN事件,不拦截不取消并且dispatchTransformedTouchEvent返回了true,那么将会不进入这个判断,如果不是,那么将会在这执行自身的dispatchTouchEvent函数并且将返回值赋于handled返回。进入else语句,在里面将其mFirstTouchTarget进行next遍历,里面的if语句则是DOWN事件下的dispatchTransformedTouchEvent返回true的情况,直接将其赋值,然后返回,里面的else语句则是,调用dispatchTransformedTouchEvent,然后将其返回值返回。

到这里,ViewGroups事件分发源码的流程就分析了,我们根据这个来说说上面的场景。

场景一:我们在TouchViewGroup的dispatchTouchEvent正常返回super,DOWN事件先触发TouchViewGroup的dispatchTouchEvent,然后就执行onInterceptTouchEvent是否拦截,onInterceptTouchEvent返回的是super,也就是false,所以就会通过dispatchTransformedTouchEvent来执行TouchView的dispatchTouchEvent,后面就是View的Touch事件分发了,View流程将会按照dispatchTouchEvent->onTouchListener - > onTouchEvent的顺序执行,因为设置了点击事件,所以在这里就返回了true,这个时候就会通过addTouchTarget()给mFirstTouchTarget赋值,下面就直接返回了true。然后在MOVE和UP事件的时候,也是首先执行dispatchTouchEvent,调用super然后调用onInterceptTouchEvent询问是否拦截,还是false,但是这里因为不是DOWN事件,所以就不会进入判断对其childView反遍历,因为在DOWN的时候mFirstTouchTarget赋值了,所以这时候进入第4步的else语句里面,这时候就对其遍历执行dispatchTransformedTouchEvent,也就是dispatchTouchEvent,然后将其返回。

场景2:我们取消了点击事件,那么在DOWN的时候就不会给mFirstTouchTarget赋值,这个时候将会进入第4步的if判断里面,直接调用dispatchTransformedTouchEvent,所以事件就不会有拦截,最终返回false,所以后续将不会接受到任何事件

场景3:我们在TouchViewGroup的时候是在onInterceptTouchEvent返回true,所以我们intercepted=true,这时候就不会给mFirstTouchTarget赋值,这个时候就调用自身的dispatchTransformedTouchEvent,同样的返回false,后续将不会接受到事件。

通过源码的角度我们也知道了为什么会这么执行,初步有点模糊,我们需要通过项目慢慢的来完善对它的认知。希望对大家有所帮助。

参考链接:
http://www.jianshu.com/p/98d1895c409d
http://www.jianshu.com/p/e99b5e8bd67b

Android Handler源码分析

发表于 2018-03-14 | 分类于 源码分析 |
字数统计: 763字 | 阅读时长 ≈ 4分钟

Handler:主要用于将Message加入到队列MessageQueue中(按照时间排序),Looper和MessageQueue中间的桥梁。

MessageQueue:Message消息队列。通过Handler加入新的Message,通过Looper循环不断调用next()拿出要消费的Message来触发Handler.dispatchMessage(msg)然后在回收消费的Message。

Looper :通过ThreadLocal来保证每个线程只有一个Looper。通过Looper.prepare();和Looper.loop();来开启死循环不断取出要消费的Message。

如下图所示:

handler

Handler.sendMessage() -->sendMessageDelyed()--> MessageQueue.enqueueMessage()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}

msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}

// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}

如下图所示:

enqueueMessage

Looper.loop():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
// 获取线程的 Looper 对象
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
// 获取队列
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

for (;;) { // 死循环不断的获取 消息队列中的消息
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}

// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}

final long traceTag = me.mTraceTag;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
try {
// 通过 handler 去执行 Message 这个时候就调用了 handleMessage 方法
msg.target.dispatchMessage(msg);
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}

if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
// 回收消息
msg.recycleUnchecked();
}
}

避免handler引起的内存泄漏问题:调用handler.removeCallbacksAndMessages(null)清空消息队列,在置handler为null。因为里面的Message被引用,所以handler只让它置为null是没有用的。

epoll唤醒分析: http://blog.csdn.net/ashqal/article/details/32107099

自定义View(8) -- 汽车之家折叠列表

发表于 2018-03-13 | 分类于 自定义view |
字数统计: 1,252字 | 阅读时长 ≈ 5分钟

先看看汽车之家折叠列表的效果图
汽车之家折叠列表

接着看看实现的效果图

实现的效果

在这篇文章中主要采用ViewDragHelper这个类,这个是系统提供的一个处理view拖动的一个类。具体请查看相关资料,在这就不多说。
先来解析实现的思路,view的移动采用ViewDragHelper即可,如果下方是一般的View的话就差不多了,但是如果是ListView或者RecyclerView之类的话主要处理一个事件拦截的逻辑。首先要清楚ListView或者RecyclerView在处理事件的时候调用了getParent().requestDisallowInterceptTouchEvent(true);请求父布局不拦截事件,所以当拦截的时候不能让ListView或者RecyclerView接受到MOVE事件。逻辑很简单,就是当下面的ListView或者RecyclerView到顶部 并且是下拉的时候就需要使用ViewDragHelper来响应拖动,如果上面的菜单是打开状态的话那么也需要响应,这时候就需要拦截MOVE事件来处理拖动。逻辑就是这么简单,但是细节的东西有很多,不能马虎并且熟悉相关的api。


接下来开始撸码
这里我选择继承FrameLayout,在初始化的时候创建ViewDragHelper,资源加载完毕了得到需要拖动的mDragView,在测量之后获取到最大拖动的距离,也就是上方菜单的高度,当手指抬起的时候判断是需要关闭还是打开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class VerticalDragListView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0)
: FrameLayout(context, attrs, defStyleAttr) {

private var mDragView: View? = null//拖动的view
private var mMenuViewHeight: Int = 0 //拖动的view 高度
private var mMenuIsOpen: Boolean = false//是否打开
private var mViewDragHelper: ViewDragHelper? = null //拖动的辅助类

private val mCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
//指定view是否可以拖动
override fun tryCaptureView(child: View, pointerId: Int): Boolean {
return mDragView == child
}

//返回移动的距离
override fun clampViewPositionVertical(child: View?, top: Int, dy: Int): Int {
//滑动的范围只能是在menu的高度
var t: Int = top
if (top <= 0) t = 0
if (top >= mMenuViewHeight) t = mMenuViewHeight
return t
}

//手松开的时候回调 打开还是关闭
override fun onViewReleased(releasedChild: View?, xvel: Float, yvel: Float) {
//打开菜单
if (mDragView!!.top >= mMenuViewHeight / 2) {
mViewDragHelper?.settleCapturedViewAt(0, mMenuViewHeight)
mMenuIsOpen = true
} else {//关闭菜单
mViewDragHelper?.settleCapturedViewAt(0, 0)
mMenuIsOpen = false
}
invalidate()
}
}

//响应滚动
override fun computeScroll() {
if (mViewDragHelper!!.continueSettling(true)) invalidate()
}


init {
mViewDragHelper = ViewDragHelper.create(this, mCallback)
}

override fun onFinishInflate() {
super.onFinishInflate()
if (childCount != 2) throw RuntimeException("childCount只能包含两个子布局")
mDragView = getChildAt(1)
}

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
if (changed) mMenuViewHeight = getChildAt(0).measuredHeight
}


override fun onTouchEvent(event: MotionEvent?): Boolean {
mViewDragHelper?.processTouchEvent(event)
return true
}

}

在这需要注意一点,当手指松开判断打开或者关闭菜单需要调用invalidate()并且重写computeScroll()函数来响应。

如果下方的view不是ListView或者RecyclerView之类的话,到这就可以了,但是实际开发中,下方一般是这种,所以就需要按照上面说的处理事件拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
   private var mDownY: Float = 0.0f
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
// 菜单打开要拦截
if (mMenuIsOpen) {
return true
}

// 向下滑动拦截,不让ListView或者RecyclerView做处理
// 谁拦截谁 父View拦截子View ,但是子 View 可以调这个方法
// requestDisallowInterceptTouchEvent 请求父View不要拦截,改变的其实就是 mGroupFlags 的值
when (ev!!.action) {
MotionEvent.ACTION_DOWN -> {
mDownY = ev.y
// 让 DragHelper 拿一个完整的事件
mViewDragHelper!!.processTouchEvent(ev)
}

MotionEvent.ACTION_MOVE -> {
val moveY = ev.y
if (moveY - mDownY > 0 && !canChildScrollUp()) {
// 向下滑动 && 滚动到了顶部,拦截不让ListView或者RecyclerView做处理
return true
}
}
}
return super.onInterceptTouchEvent(ev)
}

/**
* @return Whether it is possible for the child view of this layout to
* * scroll up. Override this if the child view is a custom view.
*/
fun canChildScrollUp(): Boolean {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mDragView is AbsListView) {
val absListView = mDragView as AbsListView
return absListView.childCount > 0 && (absListView.firstVisiblePosition > 0 || absListView.getChildAt(0)
.top < absListView.paddingTop)
} else {
return ViewCompat.canScrollVertically(mDragView, -1) || mDragView!!.scrollY > 0
}
} else {
return ViewCompat.canScrollVertically(mDragView, -1)
}
}

这里需要注意,如果不在ACTION_DOWN的时候调用mViewDragHelper.processTouchEvent(ev)的话,那么ViewDragHelper将会报错,将不会触发拖动事件

从字面意思都可以看出需要一个完整的事件,所以需要在ACTION_DOWN的时候调用ViewDragHelper.processTouchEvent(ev)


在一步步的分析之下,这个效果就慢慢的完成了。有了新需求的时候,在动手应该理清思路,然后想好使用相关的api,处理一些手势可以使用OnGestureListener,处理拖动可以使用ViewDragHelper,这些都是系统封装好的辅助类,应该要合理的利用这些辅助类。相信如果不使用这些辅助类也可以写出这些效果,但是那样的话也会浪费大量的事件和精力,而且很容易出错。

本文源码下载地址:https://github.com/ChinaZeng/CustomView

自定义View(7) -- 酷狗侧滑菜单

发表于 2018-03-12 | 分类于 自定义view |
字数统计: 2,865字 | 阅读时长 ≈ 14分钟

效果图
上一篇我们自定义了一个流式布局的ViewGroup,我们为了熟悉自定义ViewGroup,就继续自定义ViewGroup。这篇的内容是是仿照酷狗的侧滑菜单。
我们写代码之前,先想清楚是怎么实现,解析实现的步骤。实现侧滑的方式很多种,在这里我选择继承HorizontalScrollView,为什么继承这个呢?因为继承这个的话,我们就不用写childView的move meause layout,这样就节约了很大的代码量和事件,因为内部HorizontalScrollView已经封装好了。我们在这个控件里面放置两个childView,一个是menu,一个是content。然后我们处理拦截和快速滑动事件就可以了。思路想清楚了我们就开始撸码。
首先我们自定义一个属性,用于打开的时候content还有多少可以看到,也就是打开的时候menu距离右边的距离。

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SkiddingMenuLayout">
<attr name="menuRightMargin" format="dimension"/>
</declare-styleable>
</resources>

在初始化的时候我们通过menuRightMargin属性获取menu真正的宽度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public SkiddingMenuLayout(Context context) {
this(context, null);
}

public SkiddingMenuLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public SkiddingMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);


// 初始化自定义属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SkiddingMenuLayout);

float rightMargin = array.getDimension(
R.styleable.SkiddingMenuLayout_menuRightMargin, DisplayUtil.dip2px(context, 50));
// 菜单页的宽度是 = 屏幕的宽度 - 右边的一小部分距离(自定义属性)
mMenuWidth = (int) (DisplayUtil.getScreenWidth(context) - rightMargin);
array.recycle();
}

接着我们在布局加载完毕的时候我们指定menu和content的宽度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//xml 布局解析完毕回调的方法
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//指定宽高
//先拿到整体容器
ViewGroup container = (ViewGroup) getChildAt(0);

int childCount = container.getChildCount();
if (childCount != 2)
throw new RuntimeException("只能放置两个子View");
//菜单
mMenuView = container.getChildAt(0);
ViewGroup.LayoutParams meauParams = mMenuView.getLayoutParams();
meauParams.width = mMenuWidth;
//7.0一下的不加这句代码是正常的 7.0以上的必须加
mMenuView.setLayoutParams(meauParams);

//内容页
mContentView = container.getChildAt(1);
ViewGroup.LayoutParams contentParams = mContentView.getLayoutParams();
contentParams.width = DisplayUtil.getScreenWidth(getContext());
//7.0一下的不加这句代码是正常的 7.0以上的必须加
mContentView.setLayoutParams(contentParams);
}

这里有一个细节,我们在刚进入的时候,菜单默认是关闭的,所以我们需要调用scrollTo()函数移动一下位置,但是发现在onFinishInflate()函数里面调用没有作用,这个是为什么呢?因为我们在xml加载完毕之后,才会真正的执行View的绘制流程,这时候调用scrollTo()这个函数其实是执行了代码的,但是在onLaout()摆放childView的时候,又默认回到了(0,0)位置,所以我们应该在onLayout()之后调用这个函数

1
2
3
4
5
6
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//进入是关闭状态
scrollTo(mMenuWidth, 0);
}

初始化完毕了,接下来我们进行事件的拦截,MOVE的时候相应滑动事件,UP的时候判断是关闭还是打开,然后调用函数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

//手指抬起是二选一,要么关闭要么打开
@Override
public boolean onTouchEvent(MotionEvent ev) {
// 当菜单打开的时候,手指触摸右边内容部分需要关闭菜单,还需要拦截事件(打开情况下点击内容页不会响应点击事件)
if (ev.getAction() == MotionEvent.ACTION_UP) {
// 只需要管手指抬起 ,根据我们当前滚动的距离来判断
int currentScrollX = getScrollX();
if (currentScrollX > mMenuWidth / 2) {
// 关闭
closeMenu();
} else {
// 打开
openMenu();
}
return true;
}
return super.onTouchEvent(ev);
}

/**
* 打开菜单 滚动到 0 的位置
*/
private void openMenu() {
// smoothScrollTo 有动画
smoothScrollTo(0, 0);
}

/**
* 关闭菜单 滚动到 mMenuWidth 的位置
*/
private void closeMenu() {
smoothScrollTo(mMenuWidth, 0);
}

到这的话,滑动事件和打开关闭事件都完成了,接下来我们就处理一个效果的问题,这里当从左往右滑动的时候,是慢慢打开菜单,这时候content是有一个慢慢的缩放,menu有一个放大和透明度变小,而反过来关闭菜单的话就是相反的效果,content慢慢放大,menu缩小和透明度变大。这里还有一个细节,就是menu慢慢的退出和进入,滑动的距离不是和移动的距离相同的,所以这里还有一个平移。接下来重写onScrollChanged()函数,然后计算出一个梯度值来做处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 //滑动改变触发
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);

// //抽屉效果 两种一样
// ViewCompat.setTranslationX(mMenuView, l);
// ViewCompat.setX(mMenuView, l);

// Log.e("zzz", "l->" + l + " t->" + t + " oldl->" + oldl + " oldt->" + oldt);
//主要看l 手指从左往右滑动 由大变小
//计算一个梯度值 1->0
float scale = 1.0f * l / mMenuWidth;

//酷狗侧滑效果...
// //右边的缩放 最小是0.7f ,最大是1.0f
float rightScale = 0.7f + 0.3f * scale;
//设置mContentView缩放的中心点位置
ViewCompat.setPivotX(mContentView, 0);
ViewCompat.setPivotY(mContentView, mContentView.getHeight() / 2);
//设置右边缩放
ViewCompat.setScaleX(mContentView, rightScale);
ViewCompat.setScaleY(mContentView, rightScale);

//菜单
//透明度是半透明到全透明 0.5f-1.0f
float alpha = 0.5f + (1.0f - scale) * 0.5f;
ViewCompat.setAlpha(mMenuView, alpha);

//缩放 0.7-1.0
float leftScale = 0.7f + 0.3f * (1 - scale);
ViewCompat.setScaleX(mMenuView, leftScale);
ViewCompat.setScaleY(mMenuView, leftScale);

//退出按钮在右边
ViewCompat.setTranslationX(mMenuView, 0.2f * l);
}

这样的话我们就完成了效果,但是我们还有几个细节没有处理,首先是快速滑动的问题,还有一个是当打开menu的时候,点击content需要关闭菜单,而不是相应对应的事件。接下来我们对这两个问题进行处理。

快速滑动问题,这个问题我们采用GestureDetector这个类来做处理,这个类可以处理很多收拾问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

/**
* The listener that is used to notify when gestures occur.
* If you want to listen for all the different gestures then implement
* this interface. If you only want to listen for a subset it might
* be easier to extend {@link SimpleOnGestureListener}.
*/
public interface OnGestureListener {

/**
* Notified when a tap occurs with the down {@link MotionEvent}
* that triggered it. This will be triggered immediately for
* every down event. All other events should be preceded by this.
*
* @param e The down motion event.
*/
boolean onDown(MotionEvent e);

/**
* The user has performed a down {@link MotionEvent} and not performed
* a move or up yet. This event is commonly used to provide visual
* feedback to the user to let them know that their action has been
* recognized i.e. highlight an element.
*
* @param e The down motion event
*/
void onShowPress(MotionEvent e);

/**
* Notified when a tap occurs with the up {@link MotionEvent}
* that triggered it.
*
* @param e The up motion event that completed the first tap
* @return true if the event is consumed, else false
*/
boolean onSingleTapUp(MotionEvent e);

/**
* Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
* current move {@link MotionEvent}. The distance in x and y is also supplied for
* convenience.
*
* @param e1 The first down motion event that started the scrolling.
* @param e2 The move motion event that triggered the current onScroll.
* @param distanceX The distance along the X axis that has been scrolled since the last
* call to onScroll. This is NOT the distance between {@code e1}
* and {@code e2}.
* @param distanceY The distance along the Y axis that has been scrolled since the last
* call to onScroll. This is NOT the distance between {@code e1}
* and {@code e2}.
* @return true if the event is consumed, else false
*/
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);

/**
* Notified when a long press occurs with the initial on down {@link MotionEvent}
* that trigged it.
*
* @param e The initial on down motion event that started the longpress.
*/
void onLongPress(MotionEvent e);

/**
* Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
* and the matching up {@link MotionEvent}. The calculated velocity is supplied along
* the x and y axis in pixels per second.
*
* @param e1 The first down motion event that started the fling.
* @param e2 The move motion event that triggered the current onFling.
* @param velocityX The velocity of this fling measured in pixels per second
* along the x axis.
* @param velocityY The velocity of this fling measured in pixels per second
* along the y axis.
* @return true if the event is consumed, else false
*/
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}

/**
* The listener that is used to notify when a double-tap or a confirmed
* single-tap occur.
*/
public interface OnDoubleTapListener {
/**
* Notified when a single-tap occurs.
* <p>
* Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
* will only be called after the detector is confident that the user's
* first tap is not followed by a second tap leading to a double-tap
* gesture.
*
* @param e The down motion event of the single-tap.
* @return true if the event is consumed, else false
*/
boolean onSingleTapConfirmed(MotionEvent e);

/**
* Notified when a double-tap occurs.
*
* @param e The down motion event of the first tap of the double-tap.
* @return true if the event is consumed, else false
*/
boolean onDoubleTap(MotionEvent e);

/**
* Notified when an event within a double-tap gesture occurs, including
* the down, move, and up events.
*
* @param e The motion event that occurred during the double-tap gesture.
* @return true if the event is consumed, else false
*/
boolean onDoubleTapEvent(MotionEvent e);
}

/**
* The listener that is used to notify when a context click occurs. When listening for a
* context click ensure that you call {@link #onGenericMotionEvent(MotionEvent)} in
* {@link View#onGenericMotionEvent(MotionEvent)}.
*/
public interface OnContextClickListener {
/**
* Notified when a context click occurs.
*
* @param e The motion event that occurred during the context click.
* @return true if the event is consumed, else false
*/
boolean onContextClick(MotionEvent e);
}

这里我们主要是响应onFling()这个函数,然后判断当前是打开还是关闭状态,在根据快速滑动的手势来执行打开还是关闭的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 @Override
public boolean onTouchEvent(MotionEvent ev) {
if (mGestureDetector.onTouchEvent(ev))//快速滑动触发了下面的就不要执行了
return true;

//....
}


//快速滑动
private GestureDetector.OnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//快速滑动回调
//打开的时候从右到左滑动关闭 关闭的时候从左往右打开
// Log.e("zzz", "velocityX->" + velocityX);
// >0 从左往右边滑动 <0 从右到左
if (mMenuIsOpen) {
if (velocityX < 0) {
closeMenu();
return true;
}
} else {
if (velocityX > 0) {
openMenu();
return true;
}
}
return super.onFling(e1, e2, velocityX, velocityY);
}
};

接下来处理menu打开状态下点击content关闭menu,这里我们需要用到onInterceptTouchEvent。当打开状态的时候,我们就把这个事件拦截,然后关闭菜单即可。但是这里有一个问题,当我们拦截了DOWN事件之后,后面的MOVE UP事件都会被拦截并且相应自身的onTouchEvent事件,所以这里我们需要添加一个判断值,判断是否拦截,然后让其onTouchEvent是否继续执行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
isIntercept = false;
if (mMenuIsOpen && ev.getX() > mMenuWidth) {//打开状态 触摸右边关闭
isIntercept = true;//拦截的话就不执行自己的onTouchEvent
closeMenu();
return true;
}
return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent ev) {

if (isIntercept)//拦截的话就不执行自己的onTouchEvent
return true;
//...
}


根据我们提出需求,然后分析需求,再完成需求。这一步步我们慢慢进行渗透,最终完成效果,完成之后你会发现其实也就那么一回事。当我们有新需求的时候,我们应该不要恐惧,应该欣然乐观的接收,再慢慢分析,最终完成。这样的话我们才能提高我们的技术。

本文源码下载地址:https://github.com/ChinaZeng/CustomView

自定义View(6) -- 流式布局

发表于 2018-03-11 | 分类于 自定义view |
字数统计: 1,715字 | 阅读时长 ≈ 8分钟

先看效果图:
tagLayout

上几次都是自定义的view,这期我们来自定义一个简单的ViewGroup。和自定义view不同的是,viewGroup一般情况下是管理childView,所以主要是重写onMeause()来测量childView宽高,从而设置自身宽高,然后重写onLayout来摆放childView的位置,我们一般不对viewGroup进行绘制,如果特殊情况需要绘制,重写dispatchDraw()来进行重绘,因为viewGroup在不设置设置背景的情况下是不会调用onDraw()的,具体请看源码。


接下来进行我们这篇文章的主题,先理清楚思路,我们实现的效果就是一个容器里面的childView挨着挨着的从左往右摆放,如果摆放的时候这个childView的宽度加上这行前面的childView宽度大于当前设置的宽度的时候,那么就需要换行。
初始化和属性我就不说了,如需要可以自行添加,这里我们要考略到childView的margin,所以我们仿照LinearLayout来重写generateLayoutParams函数来设置一个带有margin属性的LayoutParams

1
2
3
4
5
// 设置自己需要的LayoutParams
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}

重写onMeause()来进行childView的遍历测量,并且设置自生的宽高,因为我们考略到下面要重写onLayout来摆放childView的位置,难免会有一番计算,我们为了不重复计算,所以写了一个List来进行对childView以行为单位来组装,具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
 private List<List<View>> mChildViews = new ArrayList<>();
// 指定宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 清空集合
mChildViews.clear();

int childCount = getChildCount();

// 获取到宽度
int width = MeasureSpec.getSize(widthMeasureSpec);

// 高度需要计算
int height = getPaddingTop() + getPaddingBottom();

// 一行的宽度
int lineWidth = getPaddingLeft();

ArrayList<View> childViews = new ArrayList<>();
mChildViews.add(childViews);

// 子View高度不一致的情况下
int maxHeight = 0;
for (int i = 0; i < childCount; i++) {
// for循环测量子View
View childView = getChildAt(i);
if (childView.getVisibility() == GONE) {
continue;
}
// 这段话执行之后就可以获取子View的宽高,因为会调用子View的onMeasure
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
// margin值 ViewGroup.LayoutParams 没有 就用系统的MarginLayoutParams
// LinearLayout有自己的 LayoutParams 会复写一个非常重要的方法
MarginLayoutParams params = (MarginLayoutParams) childView.getLayoutParams();
// 什么时候需要换行,一行不够的情况下 考虑 margin
if (lineWidth + (childView.getMeasuredWidth() + params.rightMargin + params.leftMargin) > width) {
// 换行,累加高度 加上一行条目中最大的高度
height += maxHeight;

//下面重新初始化
maxHeight = childView.getMeasuredHeight() + params.bottomMargin + params.topMargin;
lineWidth = childView.getMeasuredWidth() + params.rightMargin + params.leftMargin;
childViews = new ArrayList<>();
mChildViews.add(childViews);
} else {
lineWidth += childView.getMeasuredWidth() + params.rightMargin + params.leftMargin;
maxHeight = Math.max(childView.getMeasuredHeight() + params.bottomMargin + params.topMargin, maxHeight);
}
childViews.add(childView);
}
height += maxHeight;//不要忘记最后一行的高度

// Log.e("TAG", "width -> " + width + " height-> " + height);
// 根据子View计算和指定自己的宽高
setMeasuredDimension(width, height);
}

然后我们根据上面的组装的List数据来进行对childView进行摆放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left, top = getPaddingTop(), right, bottom;

for (List<View> childViews : mChildViews) {
left = getPaddingLeft();
int maxHeight = 0;
for (View childView : childViews) {
if (childView.getVisibility() == GONE) {
continue;
}
MarginLayoutParams params = (MarginLayoutParams) childView.getLayoutParams();
left += params.leftMargin;
int childTop = top + params.topMargin;
right = left + childView.getMeasuredWidth();
bottom = childTop + childView.getMeasuredHeight();
// Log.e("TAG", "left -> " + left + " top-> " + childTop + " right -> " + right + " bottom-> " + bottom);
// 摆放
childView.layout(left, childTop, right, bottom);
// left 叠加
left += childView.getMeasuredWidth() + params.rightMargin;
// 不断的叠加top值
int childHeight = childView.getMeasuredHeight() + params.topMargin + params.bottomMargin;
maxHeight = Math.max(maxHeight, childHeight);
}
top += maxHeight;
}
}

主要的逻辑代码已经写的很清楚了,细致一看就能很容易的理解。这样的画效果就实现了,但是我们在实际开发的过程中,一般是后台获取到一个List数据,然后我们在设置值,这里推荐一个设计模式就是Adapter模式。这样的话我们就可以自定义自己的View,轻松的降低了代码的耦合,并且复用效果也好,我们申明一个`Adapter’类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Email 240336124@qq.com
* Created by Darren on 2017/6/11.
* Version 1.0
* Description: 流式布局的Adapter
*/
public abstract class BaseAdapter {

// 1.有多少个条目
public abstract int getCount();

// 2.getView通过position
public abstract View getView(int position,ViewGroup parent);

}

这里就简单写了,然后我们在容器中添加setAdapter()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 设置Adapter
*
* @param adapter
*/
public void setAdapter(BaseAdapter adapter) {
if (adapter == null) {
throw new NullPointerException("adapter is null");
}
// 清空所有子View
removeAllViews();
mAdapter = adapter;
// 获取数量
int childCount = mAdapter.getCount();
for (int i = 0; i < childCount; i++) {
// 通过位置获取View
View childView = mAdapter.getView(i, this);
addView(childView);
}
}

然后我们使用就和ListView类似了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private TagLayout mTagLayout;

private List<String> mItems;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_taglayout);
mTagLayout = (TagLayout) findViewById(R.id.taglayout);

mItems = new ArrayList<>();

for (int i = 0; i < 20; i++) {
mItems.add("ABC"+i);
}
mTagLayout.setAdapter(new BaseAdapter() {
@Override
public int getCount() {
return mItems.size();
}

@Override
public View getView(int position, ViewGroup parent) {
TextView tagTv = (TextView) LayoutInflater.from(TagLayoutActivity.this)
.inflate(R.layout.item_tag, parent, false);
tagTv.setText(mItems.get(position));
// 操作ListView的方式差不多
return tagTv;
}
});
}

这样的话我们就简单的实现了流式布局的效果,如果需要单选多选之类的,在这基础上添加就可以了。
接下来再具体的总结下自定义view和自定义viewGroup的套路:

######View的自定义套路

  1. 初始化,自定义属性,获取自定义属性(配置属性)
  2. onMeasure()方法用于测量计算自己的宽高,前提是继承自View,如果是继承自系统已有的 TextView , Button,已经给你计算好了宽高,就可以跳过这个步骤(设置宽高)
  3. onDraw() 用于绘制自己的显示(View绘制)
  4. onTouch() 用于与用户交互(事件分发)

######ViewGroup的自定义套路

  1. 自定义属性,获取自定义属性,很少有这种需求(配置属性)
    2.onMeasure()方法,for循环测量子View,根据子View的宽高来计算自己的宽高(设置宽高)
  2. onLayout()用来摆放子View,前提是不是GONE的情况
  3. onDraw()一般不需要,默认情况下是不会调用,如果你要绘制需要实现dispatchDraw()方法(View绘制)
  4. 在很多情况下不会继承自ViewGroup ,往往是继承 系统已经提供好的ViewGroup 如 ViewPager ScrollView RelativeLayout,这样的话onMeause() onLayout()一般都可以跳过,相应的重写一些方法来实现自己的需求。比如侧滑菜单可以继承LinearLayout 或者RelativeLayout等来重写onInterceptTouchEvent来实现。

这篇文章到这就结束了 ,希望对大家有所提升。

下载地址:https://github.com/ChinaZeng/CustomView

自定义View(5) -- 评分控件RatingBar

发表于 2018-03-10 | 分类于 自定义view |
字数统计: 1,331字 | 阅读时长 ≈ 6分钟

先看效果图:
评分.gif
在我们画这个控件之前,我们想想一下怎么实现这个,显示对星星的处理,我们是自己绘制还是使用图片?其实都是可以的,但是我们为了更加快速的完成这个,我们选择了使用图片来完成。先自定义属性,在这个例子中我定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RatingBar">
<!--未选中引用-->
<attr name="starNormal" format="reference" />
<!--选中引用-->
<attr name="starFocus" format="reference" />
<!--最大的分数-->
<attr name="gradeNumber" format="integer" />
<!--当前的分数-->
<attr name="currentGrade" format="integer" />
<!--星星之间的间距-->
<attr name="statPadding" format="dimension" />
</declare-styleable>
</resources>

我们在初始化的时候获取相关数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 private Bitmap mStarFocusBitmap, mStarNormalBitmap;
private int mGradeNumber;//最大分数
private int mCurrentGrade;//当前分数 1开始的 最少为1分
private int mStarPadding;//间距

public RatingBar(Context context) {
this(context, null);
}

public RatingBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public RatingBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);


TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RatingBar);
int starNormalId = array.getResourceId(R.styleable.RatingBar_starNormal, 0);
if (starNormalId == 0) {
throw new RuntimeException("请设置属性 starNormal ");
}

mStarNormalBitmap = BitmapFactory.decodeResource(getResources(), starNormalId);

int starFocusId = array.getResourceId(R.styleable.RatingBar_starFocus, 0);
if (starFocusId == 0) {
throw new RuntimeException("请设置属性 starFocus ");
}

mStarFocusBitmap = BitmapFactory.decodeResource(getResources(), starFocusId);
mGradeNumber = array.getInt(R.styleable.RatingBar_gradeNumber, 0);
mStarPadding = array.getDimensionPixelOffset(R.styleable.RatingBar_statPadding, DisplayUtil.dip2px(context, 5));
mCurrentGrade = array.getInt(R.styleable.RatingBar_currentGrade, 1);

array.recycle();
}
}

接下来我们设置宽高,本例中就直接写了,在实际开发中如有需要在修改

1
2
3
4
5
6
7
8
9
10
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 高度 一张图片的高度
int height = mStarFocusBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
int width = mStarFocusBitmap.getWidth() * mGradeNumber
+ getPaddingLeft() + getPaddingRight()
+ mStarPadding * (mGradeNumber - 1);
setMeasuredDimension(width, height);
}

接下来调用onDraw函数绘制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

for (int i = 0; i < mGradeNumber; i++) {
if (i < mCurrentGrade) {
canvas.drawBitmap(mStarFocusBitmap,
getPaddingLeft() + i * mStarFocusBitmap.getWidth() + i * mStarPadding, getPaddingTop(), null);
} else {
canvas.drawBitmap(mStarNormalBitmap,
getPaddingLeft() + i * mStarFocusBitmap.getWidth() + i * mStarPadding, getPaddingTop(), null);
}
}
}

我们进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>


<com.zzw.customview.view.RatingBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:currentGrade="2"
app:gradeNumber="5"
app:statPadding="10dp"
app:starFocus="@mipmap/star_selected"
app:starNormal="@mipmap/star_normal"
/>


</RelativeLayout>

效果图如下:

这时候只是一个静态的,点击的时候并没有变化,我们接下来进行onTouchEvent监听,然后进行改变值重绘:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
//计算下标
int pos = (int) (x / (mStarFocusBitmap.getWidth() + mStarPadding));
if (pos < 0) pos = 0;
if (pos >= mGradeNumber) pos = mGradeNumber - 1;
mCurrentGrade = pos + 1;
invalidate();
break;
}
return true;
}

我们为什么要加1呢?因为我们mCurrentGrade表示的是评分的分数,所以用下标加1,这个时候我们效果就达到了,但是我们不要忘记优化。
因为我们返回了true,所以在滑动的时候一直接受到move的事件,就一直进行调用了invalidate重绘 ,我们上次也说过invalidate会做很多事情,所以我们就要减少重绘的此时,我们应该加上判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
//计算下标
int pos = (int) (x / (mStarFocusBitmap.getWidth() + mStarPadding));
if (pos < 0) pos = 0;
if (pos >= mGradeNumber) pos = mGradeNumber - 1;
if (pos != mCurrentGrade - 1) {//分数-1 等于 坐标pos
mCurrentGrade = pos + 1;
invalidate();
}
break;
}
return true;
}

这样的画就优雅了很多,当然也不要忘记bitmap的回收,我们将暴露出函数来给使用者在activity的onDestory生命周期调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 回收
*/
public void recycle() {
if (mStarFocusBitmap != null)
mStarFocusBitmap.recycle();

if (mStarNormalBitmap != null)
mStarNormalBitmap.recycle();

mStarFocusBitmap=null;
mStarNormalBitmap=null;
}

在activity里面调用:

1
2
3
4
5
@Override
protected void onDestroy() {
super.onDestroy();
mRatingBar.recycle();
}

这样的话对于强迫症患者心里就舒坦多了。当然,我也是一个强迫症换患者。但是有时候我们也不能一定要追求极致,比如在获取位置的时候,我有一个想法,就是根据星星的范围精确的获取当前的位置,比如下面这样:


当时我就写了一个小算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 计算位置
*
* @param x
* @return
*/
private int comPos(float x) {

int startX = getPaddingLeft();
if (x < startX) {
return 0;
}

if (x > getWidth() - getPaddingRight()) {
return mGradeNumber;
}

int pos = 0;
for (int i = 0; i < mGradeNumber; i++) {
int endX;
if (i == 0 || i == mGradeNumber - 1) {//第0个位置的后面的x
endX = startX + mStarFocusBitmap.getWidth() + mStarPadding / 2;
} else {
endX = startX + mStarFocusBitmap.getWidth() + mStarPadding;
}
if (x < endX) {
pos = i;
break;
}
startX = endX;
}
return pos;
}

这样的画就能够准确的拿到星星的位置,但是这样的弊端就是一个for循环,将会耗费更多的时间,所以最后还是改为了一个模糊的计算位置的方式。所以有时候也不能太强迫自己,不要钻牛角尖。
这篇文章结束了,希望对大家有所帮助!

下载链接:https://github.com/ChinaZeng/CustomView

自定义View(4) -- 字体变色

发表于 2018-03-09 | 分类于 自定义view |
字数统计: 1,856字 | 阅读时长 ≈ 9分钟

字体变色.gif

初始化我就不说了,先思考我们需要什么属性,这里我就随便写了两个,一个是变色的颜色,一个是正常的颜色,当然也可以是默认的字体颜色,我们在attr里面申明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<declare-styleable name="ColorTrackTextView">
<attr name="originColor" format="color" />
<attr name="changeColor" format="color" />
</declare-styleable>

private int mOriginColor;//不变化的颜色
private int mChangeColor;//变化的颜色
private Paint mOriginPaint, mChangePaint;

private void init(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ColorTrackTextView);
mOriginColor = ta.getColor(R.styleable.ColorTrackTextView_originColor, Color.BLACK);
mChangeColor = ta.getColor(R.styleable.ColorTrackTextView_changeColor, Color.RED);
ta.recycle();

mOriginPaint = new Paint();
mOriginPaint.setColor(mOriginColor);
mOriginPaint.setAntiAlias(true);
mOriginPaint.setTextSize(getTextSize());

mChangePaint = new Paint();
mChangePaint.setColor(mChangeColor);
mChangePaint.setAntiAlias(true);
mChangePaint.setTextSize(getTextSize());

}

因为我们选择了继承的是TextView,所以我们就不进行 onMeasure(),我们重写onDraw(),覆盖原有的绘制逻辑,我们自己来绘制,这里我们主要使用canvas.clipRect(rect)这个函数来实现裁剪,我们先来测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//思路:利用clipRect  来裁剪   使用两个画笔
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas); //不使用TextView的绘制 自己画

canvas.save();
int mid = 500;
Rect rect = new Rect(0, 0, mid, getHeight());
canvas.clipRect(rect);

String text = getText().toString();
int x = (int) (getPaddingLeft() + getWidth() / 2 - mOriginPaint.measureText(text) / 2);
int y = getPaddingTop() + DisplayUtil.getTextBaseLine(getHeight(), mOriginPaint);
canvas.drawText(text, x, y, mChangePaint);
canvas.restore();

canvas.save();
rect.set(mid, 0, getWidth(), getHeight());
canvas.clipRect(rect);
canvas.drawText(text, x, y, mOriginPaint);
canvas.restore();
}

<com.zzw.customview.view.ColorTrackTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/colortv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:text="1111111"
android:textSize="50sp"
app:changeColor="@color/colorAccent"
app:originColor="@color/colorPrimary" />

可以看到,我们是有效果的,因为我们考略到要和 ViewPager 一起配合使用,所以我们定义一个float类型的mCurrentProgress属性,用于表示当前的滑动进度,在设置一个方向,颜色是从左到右变化还是从右到左的变化

1
2
3
4
5
6
7
8
9
//不同的朝向
public static final int DIRECTION_LEFT_TO_RIGHT = 1;//从左边变色
public static final int DIRECTION_RIGHT_TO_LEFT = 2;//从右边变色


private int mDirection = DIRECTION_LEFT_TO_RIGHT;

//当前进度
private float mCurrentProgress;

接下来我们修改onDraw()以及优化一下赘余代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
 //思路:利用clipRect  来裁剪   使用两个画笔
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas); //不使用TextView的绘制 自己画

int mid = comMiddle();
if (mDirection == DIRECTION_LEFT_TO_RIGHT) {
drawText(canvas, mChangePaint, 0, mid); //画左边 颜色
drawText(canvas, mOriginPaint, mid, getWidth());//画右边
} else {
mid = getWidth() - mid;
drawText(canvas, mChangePaint, mid, getWidth());//画右边 颜色
drawText(canvas, mOriginPaint, 0, mid); //画左边
}
}

/**
* 根据当前进度算出中间值
*
* @return
*/
private int comMiddle() {
return (int) (mCurrentProgress * getWidth());
}


/**
* 根据start end 确定rect绘制文字
*
* @param canvas
* @param paint
* @param start
* @param end
*/
private void drawText(Canvas canvas, Paint paint, int start, int end) {
canvas.save();
Rect rect = new Rect(start, 0, end, getHeight());//确定区域
canvas.clipRect(rect);

String text = getText().toString();
int x = (int) (getPaddingLeft() + getWidth() / 2 - paint.measureText(text) / 2);
int y = getPaddingTop() + DisplayUtil.getTextBaseLine(getHeight(), paint);
canvas.drawText(text, x, y, paint);
canvas.restore();
}

接下来我们进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void leftToRight(View view) {
mColorTrackTextView.setDirection(ColorTrackTextView.DIRECTION_LEFT_TO_RIGHT);
startAnim(0, 1);
}

public void RightToLeft(View view) {
mColorTrackTextView.setDirection(ColorTrackTextView.DIRECTION_RIGHT_TO_LEFT);
startAnim(0, 1);
}


private void startAnim(float startPro, float endPro) {
ValueAnimator animator = ObjectAnimator.ofFloat(startPro, endPro);
animator.setDuration(2000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float currentProgress = (float) animation.getAnimatedValue();
mColorTrackTextView.setCurrentProgress(currentProgress);
}
});
animator.start();
}

就会得到如下图的效果:
字体变色测试.gif

感觉离成功近了一步,这里我们顺便优化了一个性能的问题,因为是不端的调用setCurrentProgress()方法进行重新绘制,所以这里我们加个判断:

1
2
3
4
5
6
7
public void setCurrentProgress(float currentProgress) {
if (mCurrentProgress == currentProgress)//当前进度相同就不执行下一步
return;

this.mCurrentProgress = currentProgress;
invalidate();
}

让它重复的时候不进行重绘,这只是一个小细节,接下来我们和ViewPager配合使用。我们在开发中一般遇到的是根据一个数据源,然后动态的添加Fragment和ViewPager进行关联,这里我们模拟这个场景。我们使用一个LinearLayout来管理这些字体变色的view, ViewPager的OnPageChangeListener来管理字体变色,布局为下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">

<LinearLayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:id="@+id/indicator_view"
android:layout_height="wrap_content"/>

<android.support.v4.view.ViewPager
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/view_pager"
android:layout_weight="1"
/>
</LinearLayout>

然后我们在代码中动态的添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package com.zzw.customview;

import android.graphics.Color;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import com.zzw.customview.view.ColorTrackTextView;

import java.util.ArrayList;
import java.util.List;

/**
* Created by zzw on 2017/6/15.
* Version:
* Des: 字体变色和viewpager配合使用
*/
public class ViewPagerActivity extends AppCompatActivity {
private String[] items = {"热点", "推荐", "社会", "图片", "科技", "运动"};
private LinearLayout mIndicatorContainer;// 变成通用的
private List<ColorTrackTextView> mIndicators;
private ViewPager mViewPager;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_pager);

mIndicators = new ArrayList<>();
mIndicatorContainer = (LinearLayout) findViewById(R.id.indicator_view);
mViewPager = (ViewPager) findViewById(R.id.view_pager);
initIndicator();
initViewPager();
}

/**
* 初始化ViewPager
*/
private void initViewPager() {
mViewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
@Override
public Fragment getItem(int position) {
return ItemFragment.newInstance(items[position]);
}

@Override
public int getCount() {
return items.length;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {

}
});

mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
Log.e("TAG", "position -> " + position + " positionOffset -> " + positionOffset + " positionOffsetPixels->" + positionOffsetPixels);
// position 代表当前的位置
// positionOffset 代表滚动的 0 - 1 百分比 左滑 1->0 右滑-> 0-1

// 1.左边 位置 position
ColorTrackTextView left = mIndicators.get(position);
left.setDirection(ColorTrackTextView.DIRECTION_RIGHT_TO_LEFT);
left.setCurrentProgress(1 - positionOffset);

try {
ColorTrackTextView right = mIndicators.get(position + 1);
right.setDirection(ColorTrackTextView.DIRECTION_LEFT_TO_RIGHT);
right.setCurrentProgress(positionOffset);
} catch (Exception e) {

}
}

@Override
public void onPageSelected(int position) {

}

@Override
public void onPageScrollStateChanged(int state) {

}
});
}

/**
* 初始化可变色的指示器
*/
private void initIndicator() {
for (int i = 0; i < items.length; i++) {
// 动态添加颜色跟踪的TextView
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
params.weight = 1;
ColorTrackTextView colorTrackTextView = new ColorTrackTextView(this);
// 设置颜色
colorTrackTextView.setTextSize(20);
colorTrackTextView.setChangeColor(Color.RED);
colorTrackTextView.setText(items[i]);
colorTrackTextView.setLayoutParams(params);

// 把新的加入LinearLayout容器
mIndicatorContainer.addView(colorTrackTextView);
// 加入集合
mIndicators.add(colorTrackTextView);
}
}

这里我们发现左右滑动的时候可以实现了,但是点击tab的时候还会出现上一个没有变色的情况,这里我就不上图了,我们直接写一个函数,在选中之后把重新设置颜色即可。

1
2
3
4
5
6
7
8
9
10
private void selectPos(int pos) {
for (int i = 0; i < mIndicators.size(); i++) {
ColorTrackTextView colorTrackTextView = mIndicators.get(i);
if (i == pos) {
colorTrackTextView.setCurrentProgress(1.0f);
} else {
colorTrackTextView.setCurrentProgress(0.0f);
}
}
}

最后要说的是,优化问题,在这篇文章中我们在绘制的过程中就已经进行优化了,我们要养成这种好习惯,不管是代码上还是性能上。
下载地址:https://github.com/ChinaZeng/CustomView

参考链接:http://www.jianshu.com/p/6e4b3eebbba0

1…678
曾大稳丶

曾大稳丶

80 日志
11 分类
20 标签
© 2018 — 2019 曾大稳丶
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4
访问人数 人 总访问量 次