Unity 带文本联想的 InputField
效果图
核心点
脚本继承 InputField,并在 InputField 上扩展了 Dropdown 的部分功能
- 不废话,直接上代码 ;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
public class SearchInputField : InputField
{
[SerializeField]
private RectTransform m_Template;
public RectTransform template { get { return m_Template; } set { m_Template = value; } }
[SerializeField]
private List<string> m_Options = new List<string>();
public List<string> options { get { return m_Options; } set { m_Options = value; } }
[Serializable]
public class SearchInputFieldEvent : UnityEvent<string> { }
[SerializeField]
private SearchInputFieldEvent m_OnMatched = new SearchInputFieldEvent();
public SearchInputFieldEvent onMatched { get { return m_OnMatched; } set { m_OnMatched = value; } }
private List<string> _matchResults = new List<string>();
private List<DropdownItem> _items = new List<DropdownItem>();
private bool _isValidTemplate = false;
private GameObject _dropdown;
private GameObject _blocker;
protected override void Awake()
{
base.Awake();
}
protected override void OnEnable()
{
base.OnEnable();
}
protected override void Start()
{
base.Start();
onValueChanged.AddListener(txt => { MatchTextAndShow(txt); });
}
protected override void OnDisable()
{
base.OnDisable();
}
protected override void OnDestroy()
{
base.OnDestroy();
}
private void MatchTextAndShow(string txt)
{
if (string.IsNullOrEmpty(txt))
{
Hide();
}
else
{
_matchResults.Clear();
foreach (string item in m_Options)
{
if (item.Contains(txt))
{
_matchResults.Add(item);
}
}
if (_matchResults.Count > 0)
{
Hide();
Show();
}
else
{
Hide();
}
}
}
protected internal class DropdownItem : MonoBehaviour
{
[SerializeField]
private Text m_Text;
[SerializeField]
private RectTransform m_RectTransform;
[SerializeField]
private Button m_Button;
public Text text { get { return m_Text; } set { m_Text = value; } }
public RectTransform rectTransform { get { return m_RectTransform; } set { m_RectTransform = value; } }
public Button button { get { return m_Button; } set { m_Button = value; } }
}
public void AddOption(string option)
{
m_Options.Add(option);
}
public void AddOptions(List<string> options)
{
m_Options.AddRange(options);
}
public void ClearOptions()
{
m_Options.Clear();
}
private void SetupTemplate()
{
GameObject templateGo = template.gameObject;
templateGo.SetActive(true);
Button itemBtn = template.GetComponentInChildren<Button>();
Text itemTxt = template.GetComponentInChildren<Text>();
DropdownItem item = itemBtn.gameObject.AddComponent<DropdownItem>();
item.text = itemTxt;
item.button = itemBtn;
item.rectTransform = (RectTransform)itemBtn.transform;
Canvas popupCanvas = GetOrAddComponent<Canvas>(templateGo);
popupCanvas.overrideSorting = true;
popupCanvas.sortingOrder = 30000;
GetOrAddComponent<GraphicRaycaster>(templateGo);
GetOrAddComponent<CanvasGroup>(templateGo);
templateGo.SetActive(false);
_isValidTemplate = true;
}
private static T GetOrAddComponent<T>(GameObject go) where T : Component
{
T comp = go.GetComponent<T>();
if (!comp)
comp = go.AddComponent<T>();
return comp;
}
private void Show()
{
if (!IsActive() || !IsInteractable()) return;
if (!_isValidTemplate)
{
SetupTemplate();
if (!_isValidTemplate)
return;
}
List<Canvas> list = new List<Canvas>();
gameObject.GetComponentsInParent(false, list);
if (list.Count == 0)
return;
Canvas rootCanvas = list[0];
template.gameObject.SetActive(true);
// Instantiate the drop-down template
_dropdown = Instantiate(template.gameObject);
_dropdown.name = "Dropdown List";
_dropdown.SetActive(true);
RectTransform dropdownRectTransform = _dropdown.transform as RectTransform;
dropdownRectTransform.SetParent(template.transform.parent, false);
// Find the dropdown item and disable it.
DropdownItem itemTemplate = _dropdown.GetComponentInChildren<DropdownItem>();
GameObject content = itemTemplate.rectTransform.parent.gameObject;
RectTransform contentRectTransform = content.transform as RectTransform;
itemTemplate.rectTransform.gameObject.SetActive(true);
// Get the rects of the dropdown and item
Rect dropdownContentRect = contentRectTransform.rect;
Rect itemTemplateRect = itemTemplate.rectTransform.rect;
// Calculate the visual offset between the item's edges and the background's edges
Vector2 offsetMin = itemTemplateRect.min - dropdownContentRect.min + (Vector2)itemTemplate.rectTransform.localPosition;
Vector2 offsetMax = itemTemplateRect.max - dropdownContentRect.max + (Vector2)itemTemplate.rectTransform.localPosition;
Vector2 itemSize = itemTemplateRect.size;
_items.Clear();
Button prev = null;
for (int i = 0; i < _matchResults.Count; ++i)
{
string data = _matchResults[i];
DropdownItem item = AddItem(data, itemTemplate, _items);
if (item == null)
continue;
// Automatically set up a toggle state change listener
item.button.onClick.AddListener(() =>
{
m_Text = item.text.text;
onMatched.Invoke(m_Text);
//MoveTextEnd(true);
Hide();
});
// 不要切换焦点
//item.button.Select();
// Automatically set up explicit navigation
if (prev != null)
{
Navigation prevNav = prev.navigation;
Navigation toggleNav = item.button.navigation;
prevNav.mode = Navigation.Mode.Explicit;
toggleNav.mode = Navigation.Mode.Explicit;
prevNav.selectOnDown = item.button;
prevNav.selectOnRight = item.button;
toggleNav.selectOnLeft = prev;
toggleNav.selectOnUp = prev;
prev.navigation = prevNav;
item.button.navigation = toggleNav;
}
prev = item.button;
}
// Reposition all items now that all of them have been added
Vector2 sizeDelta = contentRectTransform.sizeDelta;
sizeDelta.y = itemSize.y * _items.Count + offsetMin.y - offsetMax.y;
contentRectTransform.sizeDelta = sizeDelta;
float extraSpace = dropdownRectTransform.rect.height - contentRectTransform.rect.height;
if (extraSpace > 0)
dropdownRectTransform.sizeDelta = new Vector2(dropdownRectTransform.sizeDelta.x, dropdownRectTransform.sizeDelta.y - extraSpace);
// Invert anchoring and position if dropdown is partially or fully outside of canvas rect.
// Typically this will have the effect of placing the dropdown above the button instead of below,
// but it works as inversion regardless of initial setup.
Vector3[] corners = new Vector3[4];
dropdownRectTransform.GetWorldCorners(corners);
RectTransform rootCanvasRectTransform = rootCanvas.transform as RectTransform;
Rect rootCanvasRect = rootCanvasRectTransform.rect;
for (int axis = 0; axis < 2; axis++)
{
bool outside = false;
for (int i = 0; i < 4; i++)
{
Vector3 corner = rootCanvasRectTransform.InverseTransformPoint(corners[i]);
if (corner[axis] < rootCanvasRect.min[axis] || corner[axis] > rootCanvasRect.max[axis])
{
outside = true;
break;
}
}
if (outside)
RectTransformUtility.FlipLayoutOnAxis(dropdownRectTransform, axis, false, false);
}
for (int i = 0; i < _items.Count; i++)
{
RectTransform itemRect = _items[i].rectTransform;
itemRect.anchorMin = new Vector2(itemRect.anchorMin.x, 0);
itemRect.anchorMax = new Vector2(itemRect.anchorMax.x, 0);
itemRect.anchoredPosition = new Vector2(itemRect.anchoredPosition.x, offsetMin.y + itemSize.y * (_items.Count - 1 - i) + itemSize.y * itemRect.pivot.y);
itemRect.sizeDelta = new Vector2(itemRect.sizeDelta.x, itemSize.y);
}
// Make drop-down template and item template inactive
template.gameObject.SetActive(false);
itemTemplate.gameObject.SetActive(false);
_blocker = CreateBlocker(rootCanvas);
// 焦点
//EventSystem.current.SetSelectedGameObject(gameObject);
//MoveTextEnd(false);
}
public void Hide()
{
if (_dropdown != null)
{
for (int i = 0; i < _items.Count; i++)
{
if (_items[i] != null)
{
//DestroyItem(_items[i]);
}
}
_items.Clear();
if (_dropdown != null)
Destroy(_dropdown);
_dropdown = null;
}
if (_blocker != null)
Destroy(_blocker);
_blocker = null;
Select();
}
private GameObject CreateBlocker(Canvas rootCanvas)
{
// Create blocker GameObject.
GameObject blocker = new GameObject("Blocker");
// Setup blocker RectTransform to cover entire root canvas area.
RectTransform blockerRect = blocker.AddComponent<RectTransform>();
blockerRect.SetParent(rootCanvas.transform, false);
blockerRect.anchorMin = Vector3.zero;
blockerRect.anchorMax = Vector3.one;
blockerRect.sizeDelta = Vector2.zero;
// Make blocker be in separate canvas in same layer as dropdown and in layer just below it.
Canvas blockerCanvas = blocker.AddComponent<Canvas>();
blockerCanvas.overrideSorting = true;
Canvas dropdownCanvas = _dropdown.GetComponent<Canvas>();
blockerCanvas.sortingLayerID = dropdownCanvas.sortingLayerID;
blockerCanvas.sortingOrder = dropdownCanvas.sortingOrder - 1;
// Add raycaster since it's needed to block.
blocker.AddComponent<GraphicRaycaster>();
// Add image since it's needed to block, but make it clear.
Image blockerImage = blocker.AddComponent<Image>();
blockerImage.color = Color.clear;
// Add button since it's needed to block, and to close the dropdown when blocking area is clicked.
Button blockerButton = blocker.AddComponent<Button>();
blockerButton.onClick.AddListener(Hide);
return blocker;
}
private DropdownItem AddItem(string data, DropdownItem itemTemplate, List<DropdownItem> items)
{
// Add a new item to the dropdown.
DropdownItem item = Instantiate(itemTemplate);
item.rectTransform.SetParent(itemTemplate.rectTransform.parent, false);
item.gameObject.SetActive(true);
item.gameObject.name = "Item " + items.Count + (!string.IsNullOrEmpty(data) ? ": " + data : "");
// Set the item's data
if (item.text)
item.text.text = data;
items.Add(item);
return item;
}
}
- 为了方便创建,进行了简易的编译器扩展 ;
UI结构图
编译器自定义界面(隐藏了InputField的原始界面,可以自己修改)
using System;
using System.Reflection;
using UnityEditor;
using UnityEditor.UI;
using UnityEngine;
using UnityEngine.UI;
[CustomEditor(typeof(SearchInputField))]
public class SearchInputFieldEditor : InputFieldEditor
{
SerializedProperty m_Text;
SerializedProperty m_TextComponent;
SerializedProperty m_Placeholder;
SerializedProperty m_Template;
SerializedProperty m_Options;
SerializedProperty m_OnMatched;
protected override void OnEnable()
{
//base.OnEnable();
m_Text = serializedObject.FindProperty("m_Text");
m_TextComponent = serializedObject.FindProperty("m_TextComponent");
m_Placeholder = serializedObject.FindProperty("m_Placeholder");
m_Template = serializedObject.FindProperty("m_Template");
m_Options = serializedObject.FindProperty("m_Options");
m_OnMatched = serializedObject.FindProperty("m_OnMatched");
}
protected override void OnDisable()
{
//base.OnDisable();
}
public override void OnInspectorGUI()
{
//base.OnInspectorGUI();
serializedObject.Update();
EditorGUILayout.Space();
EditorGUILayout.PropertyField(m_Text);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(m_TextComponent);
if (m_TextComponent != null && m_TextComponent.objectReferenceValue != null)
{
Text text = m_TextComponent.objectReferenceValue as Text;
if (text.supportRichText)
{
EditorGUILayout.HelpBox("Using Rich Text with input is unsupported.", MessageType.Warning);
}
}
EditorGUILayout.PropertyField(m_Placeholder);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(m_Template);
EditorGUILayout.PropertyField(m_Options, true);
EditorGUILayout.PropertyField(m_OnMatched);
serializedObject.ApplyModifiedProperties();
}
}
public static class CustomUIControls
{
# region MenuOptions
private const string kUILayerName = "UI";
private const string kStandardSpritePath = "UI/Skin/UISprite.psd";
private const string kBackgroundSpritePath = "UI/Skin/Background.psd";
private const string kInputFieldBackgroundPath = "UI/Skin/InputFieldBackground.psd";
private const string kKnobPath = "UI/Skin/Knob.psd";
private const string kCheckmarkPath = "UI/Skin/Checkmark.psd";
private const string kDropdownArrowPath = "UI/Skin/DropdownArrow.psd";
private const string kMaskPath = "UI/Skin/UIMask.psd";
private static DefaultControls.Resources s_StandardResources;
private static DefaultControls.Resources GetStandardResources()
{
if (s_StandardResources.standard == null)
{
s_StandardResources.standard = AssetDatabase.GetBuiltinExtraResource<Sprite>(kStandardSpritePath);
s_StandardResources.background = AssetDatabase.GetBuiltinExtraResource<Sprite>(kBackgroundSpritePath);
s_StandardResources.inputField = AssetDatabase.GetBuiltinExtraResource<Sprite>(kInputFieldBackgroundPath);
s_StandardResources.knob = AssetDatabase.GetBuiltinExtraResource<Sprite>(kKnobPath);
s_StandardResources.checkmark = AssetDatabase.GetBuiltinExtraResource<Sprite>(kCheckmarkPath);
s_StandardResources.dropdown = AssetDatabase.GetBuiltinExtraResource<Sprite>(kDropdownArrowPath);
s_StandardResources.mask = AssetDatabase.GetBuiltinExtraResource<Sprite>(kMaskPath);
}
return s_StandardResources;
}
[MenuItem("GameObject/UI/Custom/Search Input Field", false, 1900)]
public static void AddSearchInputField(MenuCommand menuCommand)
{
GameObject searchInputField = CreateSearchInputField(GetStandardResources());
Assembly assembly = Assembly.Load("UnityEditor.UI");
Type type = assembly.GetType("UnityEditor.UI.MenuOptions");
BindingFlags flag = BindingFlags.Static | BindingFlags.NonPublic;
MethodInfo method = type.GetMethod("PlaceUIElementRoot", flag);
method.Invoke(null, new object[] { searchInputField, menuCommand });
}
#endregion
#region DefaultControls
private const float kWidth = 160f;
private const float kThickHeight = 30f;
private const float kThinHeight = 20f;
private static Vector2 s_ThickElementSize = new Vector2(kWidth, kThickHeight);
//private static Vector2 s_ThinElementSize = new Vector2(kWidth, kThinHeight);
//private static Vector2 s_ImageElementSize = new Vector2(100f, 100f);
private static Color s_DefaultSelectableColor = new Color(1f, 1f, 1f, 1f);
//private static Color s_PanelColor = new Color(1f, 1f, 1f, 0.392f);
private static Color s_TextColor = new Color(50f / 255f, 50f / 255f, 50f / 255f, 1f);
/// <summary>
/// Create the basic UI search input field.
/// </summary>
/// <remarks>
/// Hierarchy:
/// (root)
/// SearchInputField
/// - PlaceHolder
/// - Text
/// - Template
/// - Viewport
/// - Content
/// - Item
/// - Item Background
/// - Item Label
/// - Scrollbar
/// - Sliding Area
/// - Handle
/// <remarks>
/// <param name="resources">The resources to use for creation.</param>
/// <returns>The root GameObject of the created element.</returns>
public static GameObject CreateSearchInputField(DefaultControls.Resources resources)
{
GameObject root = CreateUIElementRoot("SearchInputField", s_ThickElementSize);
// InputField
GameObject childPlaceholder = CreateUIObject("Placeholder", root);
GameObject childText = CreateUIObject("Text", root);
Image image = root.AddComponent<Image>();
image.sprite = resources.inputField;
image.type = Image.Type.Sliced;
image.color = s_DefaultSelectableColor;
SearchInputField inputField = root.AddComponent<SearchInputField>();
inputField.targetGraphic = image;
SetDefaultColorTransitionValues(inputField);
Text text = childText.AddComponent<Text>();
text.text = "";
text.supportRichText = false;
SetDefaultTextValues(text);
Text placeholder = childPlaceholder.AddComponent<Text>();
placeholder.text = "Enter text...";
placeholder.fontStyle = FontStyle.Italic;
// Make placeholder color half as opaque as normal text color.
Color placeholderColor = text.color;
placeholderColor.a *= 0.5f;
placeholder.color = placeholderColor;
RectTransform textRectTransform = childText.GetComponent<RectTransform>();
textRectTransform.anchorMin = Vector2.zero;
textRectTransform.anchorMax = Vector2.one;
textRectTransform.sizeDelta = Vector2.zero;
textRectTransform.offsetMin = new Vector2(10, 6);
textRectTransform.offsetMax = new Vector2(-10, -7);
RectTransform placeholderRectTransform = childPlaceholder.GetComponent<RectTransform>();
placeholderRectTransform.anchorMin = Vector2.zero;
placeholderRectTransform.anchorMax = Vector2.one;
placeholderRectTransform.sizeDelta = Vector2.zero;
placeholderRectTransform.offsetMin = new Vector2(10, 6);
placeholderRectTransform.offsetMax = new Vector2(-10, -7);
inputField.textComponent = text;
inputField.placeholder = placeholder;
// Template
GameObject template = CreateUIObject("Template", root);
GameObject viewport = CreateUIObject("Viewport", template);
GameObject content = CreateUIObject("Content", viewport);
GameObject item = CreateUIObject("Item", content);
GameObject itemBackground = CreateUIObject("Item Background", item);
GameObject itemLabel = CreateUIObject("Item Label", item);
GameObject scrollbar = DefaultControls.CreateScrollbar(resources);
scrollbar.name = "Scrollbar";
SetParentAndAlign(scrollbar, template);
Scrollbar scrollbarScrollbar = scrollbar.GetComponent<Scrollbar>();
scrollbarScrollbar.SetDirection(Scrollbar.Direction.BottomToTop, true);
RectTransform vScrollbarRT = scrollbar.GetComponent<RectTransform>();
vScrollbarRT.anchorMin = Vector2.right;
vScrollbarRT.anchorMax = Vector2.one;
vScrollbarRT.pivot = Vector2.one;
vScrollbarRT.sizeDelta = new Vector2(vScrollbarRT.sizeDelta.x, 0);
// Setup item UI components.
Text itemLabelText = itemLabel.AddComponent<Text>();
SetDefaultTextValues(itemLabelText);
itemLabelText.alignment = TextAnchor.MiddleLeft;
Image itemBackgroundImage = itemBackground.AddComponent<Image>();
itemBackgroundImage.color = new Color32(245, 245, 245, 255);
Button itemButton = item.AddComponent<Button>();
itemButton.targetGraphic = itemBackgroundImage;
// Setup template UI components.
Image templateImage = template.AddComponent<Image>();
templateImage.sprite = resources.standard;
templateImage.type = Image.Type.Sliced;
ScrollRect templateScrollRect = template.AddComponent<ScrollRect>();
templateScrollRect.content = (RectTransform)content.transform;
templateScrollRect.viewport = (RectTransform)viewport.transform;
templateScrollRect.horizontal = false;
templateScrollRect.movementType = ScrollRect.MovementType.Clamped;
templateScrollRect.verticalScrollbar = scrollbarScrollbar;
templateScrollRect.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHideAndExpandViewport;
templateScrollRect.verticalScrollbarSpacing = -3;
Mask scrollRectMask = viewport.AddComponent<Mask>();
scrollRectMask.showMaskGraphic = false;
Image viewportImage = viewport.AddComponent<Image>();
viewportImage.sprite = resources.mask;
viewportImage.type = Image.Type.Sliced;
// Setting default Item list.
itemLabelText.text = "Option A";
inputField.AddOption("Option A");
inputField.AddOption("Option B");
inputField.AddOption("Option C");
// Set up RectTransforms.
RectTransform templateRT = template.GetComponent<RectTransform>();
templateRT.anchorMin = new Vector2(0, 0);
templateRT.anchorMax = new Vector2(1, 0);
templateRT.pivot = new Vector2(0.5f, 1);
templateRT.anchoredPosition = new Vector2(0, 2);
templateRT.sizeDelta = new Vector2(0, 150);
RectTransform viewportRT = viewport.GetComponent<RectTransform>();
viewportRT.anchorMin = new Vector2(0, 0);
viewportRT.anchorMax = new Vector2(1, 1);
viewportRT.sizeDelta = new Vector2(-18, 0);
viewportRT.pivot = new Vector2(0, 1);
RectTransform contentRT = content.GetComponent<RectTransform>();
contentRT.anchorMin = new Vector2(0f, 1);
contentRT.anchorMax = new Vector2(1f, 1);
contentRT.pivot = new Vector2(0.5f, 1);
contentRT.anchoredPosition = new Vector2(0, 0);
contentRT.sizeDelta = new Vector2(0, 28);
RectTransform itemRT = item.GetComponent<RectTransform>();
itemRT.anchorMin = new Vector2(0, 0.5f);
itemRT.anchorMax = new Vector2(1, 0.5f);
itemRT.sizeDelta = new Vector2(0, 20);
RectTransform itemBackgroundRT = itemBackground.GetComponent<RectTransform>();
itemBackgroundRT.anchorMin = Vector2.zero;
itemBackgroundRT.anchorMax = Vector2.one;
itemBackgroundRT.sizeDelta = Vector2.zero;
RectTransform itemLabelRT = itemLabel.GetComponent<RectTransform>();
itemLabelRT.anchorMin = Vector2.zero;
itemLabelRT.anchorMax = Vector2.one;
itemLabelRT.offsetMin = new Vector2(20, 1);
itemLabelRT.offsetMax = new Vector2(-10, -2);
template.SetActive(false);
inputField.template = templateRT;
return root;
}
private static GameObject CreateUIElementRoot(string name, Vector2 size)
{
GameObject child = new GameObject(name);
RectTransform rectTransform = child.AddComponent<RectTransform>();
rectTransform.sizeDelta = size;
return child;
}
private static GameObject CreateUIObject(string name, GameObject parent)
{
GameObject go = new GameObject(name);
go.AddComponent<RectTransform>();
SetParentAndAlign(go, parent);
return go;
}
private static void SetParentAndAlign(GameObject child, GameObject parent)
{
if (parent == null)
return;
child.transform.SetParent(parent.transform, false);
SetLayerRecursively(child, parent.layer);
}
private static void SetLayerRecursively(GameObject go, int layer)
{
go.layer = layer;
Transform t = go.transform;
for (int i = 0; i < t.childCount; i++)
SetLayerRecursively(t.GetChild(i).gameObject, layer);
}
private static void SetDefaultColorTransitionValues(Selectable slider)
{
ColorBlock colors = slider.colors;
colors.highlightedColor = new Color(0.882f, 0.882f, 0.882f);
colors.pressedColor = new Color(0.698f, 0.698f, 0.698f);
colors.disabledColor = new Color(0.521f, 0.521f, 0.521f);
}
private static void SetDefaultTextValues(Text lbl)
{
// Set text values we want across UI elements in default controls.
// Don't set values which are the same as the default values for the Text component,
// since there's no point in that, and it's good to keep them as consistent as possible.
lbl.color = s_TextColor;
// Reset() is not called when playing. We still want the default font to be assigned
//lbl.AssignDefaultFont();
}
#endregion
}
- 使用方法 ;
// 组件引用
public SearchInputField searchInputField;
// 添加联想词组
string[] strs = new string[] { "AAA", "BBB", "CCC" };
searchInputField.ClearOptions();
searchInputField.AddOptions(strs);
// 侦听选择匹配词组的事件
searchInputField.onMatched.AddListener(txt => { Debug.Log(txt); });